mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-13 04:28:55 +01:00
Rewrite tag searching to use SQL
Fix EHentai/ExHentai Fix hitomi.la Fix hitomi.la crashing application Rewrite hitomi.la search engine to be faster, use less CPU and require no preloading Fix nhentai Add additional filters to nhentai Fix PervEden Introduce delegated sources Rewrite HentaiCafe to be a delegated source Introduce ability to save/load search presets Temporarily disable misbehaving native Tachiyomi migrations Fix tap-to-search-tag breaking on aliased tags Add debug menu Add experimental automatic captcha solver Add app name to wakelock names Add ability to interrupt metadata migrator Fix incognito open-in-browser being zoomed in immediately when it's opened
This commit is contained in:
@@ -8,6 +8,8 @@ import java.io.File
|
||||
|
||||
object Migrations {
|
||||
|
||||
// TODO NATIVE TACHIYOMI MIGRATIONS ARE FUCKED UP DUE TO DIFFERING VERSION NUMBERS
|
||||
|
||||
/**
|
||||
* Performs a migration when the application is updated.
|
||||
*
|
||||
|
||||
@@ -6,18 +6,28 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.database.queries.*
|
||||
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
||||
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
||||
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
||||
import exh.metadata.sql.models.SearchMetadata
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import exh.metadata.sql.queries.SearchMetadataQueries
|
||||
import exh.metadata.sql.queries.SearchTagQueries
|
||||
import exh.metadata.sql.queries.SearchTitleQueries
|
||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
|
||||
/**
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context)
|
||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||
|
||||
: 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))
|
||||
@@ -27,6 +37,11 @@ open class DatabaseHelper(context: Context)
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
// EXH -->
|
||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||
// EXH <--
|
||||
.build()
|
||||
|
||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||
|
||||
@@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import eu.kanade.tachiyomi.data.database.tables.*
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.metadata.sql.tables.SearchTagTable
|
||||
import exh.metadata.sql.tables.SearchTitleTable
|
||||
|
||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
|
||||
@@ -18,7 +18,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 8
|
||||
const val DATABASE_VERSION = 9 // [EXH]
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@@ -28,6 +28,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(CategoryTable.createTableQuery)
|
||||
execSQL(MangaCategoryTable.createTableQuery)
|
||||
execSQL(HistoryTable.createTableQuery)
|
||||
// EXH -->
|
||||
execSQL(SearchMetadataTable.createTableQuery)
|
||||
execSQL(SearchTagTable.createTableQuery)
|
||||
execSQL(SearchTitleTable.createTableQuery)
|
||||
// EXH <--
|
||||
|
||||
// DB indexes
|
||||
execSQL(MangaTable.createUrlIndexQuery)
|
||||
@@ -35,6 +40,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||
// EXH -->
|
||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -67,6 +80,21 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
db.execSQL(MangaTable.createLibraryIndexQuery)
|
||||
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||
}
|
||||
// EXH -->
|
||||
if (oldVersion < 9) {
|
||||
db.execSQL(SearchMetadataTable.createTableQuery)
|
||||
db.execSQL(SearchTagTable.createTableQuery)
|
||||
db.execSQL(SearchTitleTable.createTableQuery)
|
||||
|
||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
}
|
||||
// Remember to increment any Tachiyomi database upgrades after this
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
// [EXH]
|
||||
class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_URL, manga.url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -157,15 +157,7 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_ts_aspNetCookie = "eh_ts_aspNetCookie"
|
||||
|
||||
const val eh_showSettingsUploadWarning = "eh_showSettingsUploadWarning1"
|
||||
|
||||
const val eh_hl_earlyRefresh = "eh_hl_early_refresh"
|
||||
|
||||
const val eh_hl_refreshFrequency = "eh_hl_refresh_frequency"
|
||||
|
||||
const val eh_hl_lastRefresh = "eh_hl_last_refresh"
|
||||
|
||||
const val eh_hl_lastRealmIndex = "eh_hl_lastRealmIndex"
|
||||
const val eh_showSettingsUploadWarning = "eh_showSettingsUploadWarning2"
|
||||
|
||||
const val eh_expandFilters = "eh_expand_filters"
|
||||
|
||||
@@ -182,4 +174,8 @@ object PreferenceKeys {
|
||||
const val eh_preserveReadingPosition = "eh_preserve_reading_position"
|
||||
|
||||
const val eh_incogWebview = "eh_incognito_webview"
|
||||
|
||||
const val eh_autoSolveCaptchas = "eh_autosolve_captchas"
|
||||
|
||||
const val eh_delegateSources = "eh_delegate_sources"
|
||||
}
|
||||
|
||||
@@ -189,16 +189,16 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun thumbnailRows() = rxPrefs.getString("ex_thumb_rows", "tr_2")
|
||||
|
||||
fun migrateLibraryAsked2() = rxPrefs.getBoolean("ex_migrate_library2", false)
|
||||
fun migrateLibraryAsked() = rxPrefs.getBoolean("ex_migrate_library3", false)
|
||||
|
||||
fun migrationStatus() = rxPrefs.getInteger("migration_status", MigrationStatus.NOT_INITIALIZED)
|
||||
|
||||
fun hasPerformedURLMigration() = rxPrefs.getBoolean("performed_url_migration", false)
|
||||
|
||||
//EH Cookies
|
||||
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
|
||||
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
|
||||
fun igneousVal() = rxPrefs.getString("eh_igneous", null)
|
||||
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", "")
|
||||
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", "")
|
||||
fun igneousVal() = rxPrefs.getString("eh_igneous", "")
|
||||
fun eh_ehSettingsProfile() = rxPrefs.getInteger(Keys.eh_ehSettingsProfile, -1)
|
||||
fun eh_exhSettingsProfile() = rxPrefs.getInteger(Keys.eh_exhSettingsProfile, -1)
|
||||
fun eh_settingsKey() = rxPrefs.getString(Keys.eh_settingsKey, "")
|
||||
@@ -228,16 +228,6 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun eh_showSettingsUploadWarning() = rxPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true)
|
||||
|
||||
// Default is 24h, refresh daily
|
||||
fun eh_hl_earlyRefresh() = rxPrefs.getBoolean(Keys.eh_hl_earlyRefresh, false)
|
||||
|
||||
fun eh_hl_refreshFrequency() = rxPrefs.getString(Keys.eh_hl_refreshFrequency, "24")
|
||||
|
||||
fun eh_hl_lastRefresh() = rxPrefs.getLong(Keys.eh_hl_lastRefresh, 0L)
|
||||
|
||||
fun eh_hl_lastRealmIndex() = rxPrefs.getInteger(Keys.eh_hl_lastRealmIndex, -1)
|
||||
// <-- EH
|
||||
|
||||
fun eh_expandFilters() = rxPrefs.getBoolean(Keys.eh_expandFilters, false)
|
||||
|
||||
fun eh_readerThreads() = rxPrefs.getInteger(Keys.eh_readerThreads, 2)
|
||||
@@ -253,4 +243,12 @@ class PreferencesHelper(val context: Context) {
|
||||
fun eh_incogWebview() = rxPrefs.getBoolean(Keys.eh_incogWebview, false)
|
||||
|
||||
fun eh_askCategoryOnLongPress() = rxPrefs.getBoolean(Keys.eh_askCategoryOnLongPress, false)
|
||||
|
||||
fun eh_autoSolveCaptchas() = rxPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false)
|
||||
|
||||
fun eh_delegateSources() = rxPrefs.getBoolean(Keys.eh_delegateSources, true)
|
||||
|
||||
fun eh_lastVersionCode() = rxPrefs.getInteger("eh_last_version_code", 0)
|
||||
|
||||
fun eh_savedSearches() = rxPrefs.getString("eh_saved_searches", "")
|
||||
}
|
||||
|
||||
@@ -22,8 +22,12 @@ import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.metadata.models.PervEdenLang
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.source.EnhancedHttpSource
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
@@ -66,8 +70,17 @@ open class SourceManager(private val context: Context) {
|
||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||
|
||||
internal fun registerSource(source: Source, overwrite: Boolean = false) {
|
||||
val sourceQName = source::class.qualifiedName
|
||||
val delegate = DELEGATED_SOURCES[sourceQName]
|
||||
val newSource = if(source is HttpSource && delegate != null) {
|
||||
Timber.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
||||
EnhancedHttpSource(
|
||||
source,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 1 }!!.call(source)
|
||||
)
|
||||
} else source
|
||||
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
||||
sourcesMap[source.id] = source
|
||||
sourcesMap[source.id] = newSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +112,8 @@ open class SourceManager(private val context: Context) {
|
||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
|
||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
|
||||
exSrcs += NHentai(context)
|
||||
exSrcs += HentaiCafe()
|
||||
exSrcs += Tsumino(context)
|
||||
exSrcs += Hitomi(context)
|
||||
exSrcs += Hitomi()
|
||||
return exSrcs
|
||||
}
|
||||
|
||||
@@ -130,4 +142,20 @@ open class SourceManager(private val context: Context) {
|
||||
return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DELEGATED_SOURCES = listOf(
|
||||
DelegatedSource(
|
||||
"Hentai Cafe",
|
||||
260868874183818481,
|
||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||
HentaiCafe::class
|
||||
)
|
||||
).associateBy { it.originalSourcePackageName }
|
||||
|
||||
data class DelegatedSource(val sourceName: String,
|
||||
val sourceId: Long,
|
||||
val originalSourcePackageName: String,
|
||||
val newSourceClass: KClass<out DelegatedHttpSource>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,102 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.models.GalleryQuery
|
||||
import exh.metadata.models.SearchableGalleryMetadata
|
||||
import exh.util.createUUIDObj
|
||||
import exh.util.defRealm
|
||||
import exh.util.realmTrans
|
||||
import rx.Observable
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* LEWD!
|
||||
*/
|
||||
interface LewdSource<M : SearchableGalleryMetadata, I> : CatalogueSource {
|
||||
fun queryAll(): GalleryQuery<M>
|
||||
interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
val db: DatabaseHelper get() = Injekt.get()
|
||||
|
||||
fun queryFromUrl(url: String): GalleryQuery<M>
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
val metaClass: KClass<M>
|
||||
|
||||
val metaParser: M.(I) -> Unit
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
fun parseIntoMetadata(metadata: M, input: I)
|
||||
|
||||
fun parseToManga(query: GalleryQuery<M>, input: I): SManga
|
||||
= realmTrans { realm ->
|
||||
val meta = realm.copyFromRealm(query.query(realm).findFirst()
|
||||
?: realm.createUUIDObj(queryAll().clazz.java))
|
||||
/**
|
||||
* Use reflection to create a new instance of metadata
|
||||
*/
|
||||
private fun newMetaInstance() = metaClass.constructors.find {
|
||||
it.parameters.isEmpty()
|
||||
}?.call() ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
|
||||
metaParser(meta, input)
|
||||
|
||||
realm.copyToRealmOrUpdate(meta)
|
||||
|
||||
SManga.create().apply {
|
||||
meta.copyTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun lazyLoadMeta(query: GalleryQuery<M>, parserInput: Observable<I>): Observable<M> {
|
||||
return defRealm { realm ->
|
||||
val possibleOutput = query.query(realm).findFirst()
|
||||
|
||||
if(possibleOutput == null)
|
||||
parserInput.map {
|
||||
realmTrans { realm ->
|
||||
val meta = realm.createUUIDObj(queryAll().clazz.java)
|
||||
|
||||
metaParser(meta, it)
|
||||
|
||||
realm.copyFromRealm(meta)
|
||||
/**
|
||||
* Parses metadata from the input and then copies it into the manga
|
||||
*
|
||||
* Will also save the metadata to the DB if possible
|
||||
*/
|
||||
fun parseToManga(manga: SManga, input: I): Completable {
|
||||
val mangaId = (manga as? Manga)?.id
|
||||
val metaObservable = if(mangaId != null) {
|
||||
db.getFlatMetadataForManga(mangaId).asRxSingle()
|
||||
.map {
|
||||
if(it != null) it.raise(metaClass)
|
||||
else newMetaInstance()
|
||||
}
|
||||
}
|
||||
else
|
||||
Observable.just(realm.copyFromRealm(possibleOutput))
|
||||
} else {
|
||||
Single.just(newMetaInstance())
|
||||
}
|
||||
|
||||
return metaObservable.map {
|
||||
parseIntoMetadata(it, input)
|
||||
it.copyTo(manga)
|
||||
it
|
||||
}.flatMapCompletable {
|
||||
if(mangaId != null) {
|
||||
it.mangaId = mangaId
|
||||
db.insertFlatMetadata(it.flatten())
|
||||
} else Completable.complete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||
* producer and parses the metadata from the input
|
||||
*
|
||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||
* also be saved to the DB.
|
||||
*/
|
||||
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> {
|
||||
val metaObservable = if(mangaId != null) {
|
||||
db.getFlatMetadataForManga(mangaId).asRxSingle()
|
||||
.map {
|
||||
it?.raise(metaClass)
|
||||
}
|
||||
} else Single.just(null)
|
||||
|
||||
return metaObservable.flatMap { existingMeta ->
|
||||
if(existingMeta == null) {
|
||||
inputProducer().flatMap { input ->
|
||||
val newMeta = newMetaInstance()
|
||||
parseIntoMetadata(newMeta, input)
|
||||
val newMetaSingle = Single.just(newMeta)
|
||||
if(mangaId != null) {
|
||||
newMeta.mangaId = mangaId
|
||||
db.insertFlatMetadata(newMeta.flatten()).andThen(newMetaSingle)
|
||||
} else newMetaSingle
|
||||
}
|
||||
} else Single.just(existingMeta)
|
||||
}
|
||||
}
|
||||
|
||||
val SManga.id get() = (this as? Manga)?.id
|
||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.EX_DATE_FORMAT
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.models.ExGalleryMetadata
|
||||
import exh.metadata.models.Tag
|
||||
import exh.metadata.nullIfBlank
|
||||
import exh.metadata.parseHumanReadableByteCount
|
||||
import exh.ui.login.LoginController
|
||||
@@ -28,10 +32,12 @@ import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
|
||||
class EHentai(override val id: Long,
|
||||
val exh: Boolean,
|
||||
val context: Context) : HttpSource(), LewdSource<ExGalleryMetadata, Response> {
|
||||
val context: Context) : HttpSource(), LewdSource<EHentaiSearchMetadata, Response> {
|
||||
override val metaClass = EHentaiSearchMetadata::class
|
||||
|
||||
val schema: String
|
||||
get() = if(prefs.secureEXH().getOrDefault())
|
||||
@@ -60,26 +66,29 @@ class EHentai(override val id: Long,
|
||||
|
||||
fun extendedGenericMangaParse(doc: Document)
|
||||
= with(doc) {
|
||||
//Parse mangas
|
||||
val parsedMangas = select(".gtr0,.gtr1").map {
|
||||
// Parse mangas (supports compact + extended layout)
|
||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||
// Do not parse header and ads
|
||||
it.selectFirst("th") == null && it.selectFirst(".itd") == null
|
||||
}.map {
|
||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||
|
||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||
|
||||
ParsedManga(
|
||||
fav = parseFavoritesStyle(it.select(".itd .it3 > .i[id]").first()?.attr("style")),
|
||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||
favElement?.attr("style")?.substring(14, 17)
|
||||
),
|
||||
manga = Manga.create(id).apply {
|
||||
//Get title
|
||||
it.select(".itd .it5 a").first()?.apply {
|
||||
title = text()
|
||||
url = ExGalleryMetadata.normalizeUrl(attr("href"))
|
||||
}
|
||||
title = thumbnailElement.attr("title")
|
||||
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
||||
//Get image
|
||||
it.select(".itd .it2").first()?.apply {
|
||||
children().first()?.let {
|
||||
thumbnail_url = it.attr("src")
|
||||
} ?: let {
|
||||
text().split("~").apply {
|
||||
thumbnail_url = "http://${this[1]}/${this[2]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail_url = thumbnailElement.attr("src")
|
||||
|
||||
// TODO Parse genre + uploader + tags
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,14 +106,6 @@ class EHentai(override val id: Long,
|
||||
Pair(parsedMangas, hasNextPage)
|
||||
}
|
||||
|
||||
fun parseFavoritesStyle(style: String?): Int {
|
||||
val offset = style?.substringAfterLast("background-position:0px ")
|
||||
?.removeSuffix("px; cursor:pointer")
|
||||
?.toIntOrNull() ?: return -1
|
||||
|
||||
return (offset + 2)/-19
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of galleries
|
||||
*/
|
||||
@@ -158,17 +159,17 @@ class EHentai(override val id: Long,
|
||||
override fun popularMangaRequest(page: Int) = if(exh)
|
||||
latestUpdatesRequest(page)
|
||||
else
|
||||
exGet("$baseUrl/toplist.php?tl=15", page)
|
||||
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
|
||||
|
||||
//Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query, {
|
||||
urlImportFetchSearchManga(query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
} .map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
|
||||
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
|
||||
@@ -229,82 +230,112 @@ class EHentai(override val id: Long,
|
||||
}!!
|
||||
|
||||
/**
|
||||
* Parse gallery page to metadata model
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return parseToManga(queryFromUrl(response.request().url().toString()), response)
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
override val metaParser: ExGalleryMetadata.(Response) -> Unit = { response ->
|
||||
with(response.asJsoup()) {
|
||||
url = response.request().url().encodedPath()!!
|
||||
gId = ExGalleryMetadata.galleryId(url!!)
|
||||
gToken = ExGalleryMetadata.galleryToken(url!!)
|
||||
/**
|
||||
* Parse gallery page to metadata model
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
exh = this@EHentai.exh
|
||||
title = select("#gn").text().nullIfBlank()?.trim()
|
||||
override fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Response) {
|
||||
with(metadata) {
|
||||
with(input.asJsoup()) {
|
||||
val url = input.request().url().encodedPath()!!
|
||||
gId = ExGalleryMetadata.galleryId(url)
|
||||
gToken = ExGalleryMetadata.galleryToken(url)
|
||||
|
||||
altTitle = select("#gj").text().nullIfBlank()?.trim()
|
||||
exh = this@EHentai.exh
|
||||
title = select("#gn").text().nullIfBlank()?.trim()
|
||||
|
||||
thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
|
||||
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
|
||||
}
|
||||
genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/')
|
||||
altTitle = select("#gj").text().nullIfBlank()?.trim()
|
||||
|
||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||
|
||||
//Parse the table
|
||||
select("#gdd tr").forEach {
|
||||
it.select(".gdt1")
|
||||
.text()
|
||||
thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
|
||||
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
|
||||
}
|
||||
genre = select(".cs")
|
||||
.attr("onclick")
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.let { left ->
|
||||
it.select(".gdt2")
|
||||
.text()
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.let { right ->
|
||||
ignore {
|
||||
when (left.removeSuffix(":")
|
||||
.toLowerCase()) {
|
||||
"posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
|
||||
"visible" -> visible = right.nullIfBlank()
|
||||
"language" -> {
|
||||
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
|
||||
translated = right.endsWith(TR_SUFFIX, true)
|
||||
}
|
||||
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
|
||||
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
|
||||
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'")
|
||||
|
||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||
|
||||
//Parse the table
|
||||
select("#gdd tr").forEach {
|
||||
val left = it.select(".gdt1").text().nullIfBlank()?.trim()
|
||||
val rightElement = it.selectFirst(".gdt2")
|
||||
val right = rightElement.text().nullIfBlank()?.trim()
|
||||
if(left != null && right != null) {
|
||||
ignore {
|
||||
when (left.removeSuffix(":")
|
||||
.toLowerCase()) {
|
||||
"posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
|
||||
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
|
||||
"parent" -> parent = if (!right.equals("None", true)) {
|
||||
rightElement.child(0).attr("href")
|
||||
} else null
|
||||
"visible" -> visible = right.nullIfBlank()
|
||||
"language" -> {
|
||||
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
|
||||
translated = right.endsWith(TR_SUFFIX, true)
|
||||
}
|
||||
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
|
||||
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
|
||||
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Parse ratings
|
||||
ignore {
|
||||
averageRating = select("#rating_label")
|
||||
.text()
|
||||
.removePrefix("Average:")
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toDouble()
|
||||
ratingCount = select("#rating_count")
|
||||
.text()
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toInt()
|
||||
}
|
||||
//Parse ratings
|
||||
ignore {
|
||||
averageRating = select("#rating_label")
|
||||
.text()
|
||||
.removePrefix("Average:")
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toDouble()
|
||||
ratingCount = select("#rating_count")
|
||||
.text()
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toInt()
|
||||
}
|
||||
|
||||
//Parse tags
|
||||
tags.clear()
|
||||
select("#taglist tr").forEach {
|
||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||
tags.addAll(it.select("div").map {
|
||||
Tag(namespace, it.text().trim(), it.hasClass("gtl"))
|
||||
})
|
||||
//Parse tags
|
||||
tags.clear()
|
||||
select("#taglist tr").forEach {
|
||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||
tags.addAll(it.select("div").map { element ->
|
||||
RaisedTag(
|
||||
namespace,
|
||||
element.text().trim(),
|
||||
if(element.hasClass("gtl"))
|
||||
TAG_TYPE_LIGHT
|
||||
else
|
||||
TAG_TYPE_NORMAL
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Add genre as virtual tag
|
||||
genre?.let {
|
||||
tags.add(RaisedTag(EH_GENRE_NAMESPACE, it, TAG_TYPE_VIRTUAL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,8 +423,11 @@ class EHentai(override val id: Long,
|
||||
cookies["hath_perks"] = hathPerksCookie
|
||||
}
|
||||
|
||||
//Session-less list display mode (for users without ExHentai)
|
||||
cookies["sl"] = "dm_0"
|
||||
// Session-less extended display mode (for users without ExHentai)
|
||||
cookies["sl"] = "dm_2"
|
||||
|
||||
// Ignore all content warnings
|
||||
cookies["nw"] = "1"
|
||||
|
||||
return cookies
|
||||
}
|
||||
@@ -431,23 +465,26 @@ class EHentai(override val id: Long,
|
||||
ReverseFilter()
|
||||
)
|
||||
|
||||
class GenreOption(name: String, val genreId: String): Filter.CheckBox(name, false), UriFilter {
|
||||
class GenreOption(name: String, val genreId: Int): Filter.CheckBox(name, false)
|
||||
class GenreGroup : Filter.Group<GenreOption>("Genres", listOf(
|
||||
GenreOption("Dōjinshi", 2),
|
||||
GenreOption("Manga", 4),
|
||||
GenreOption("Artist CG", 8),
|
||||
GenreOption("Game CG", 16),
|
||||
GenreOption("Western", 512),
|
||||
GenreOption("Non-H", 256),
|
||||
GenreOption("Image Set", 32),
|
||||
GenreOption("Cosplay", 64),
|
||||
GenreOption("Asian Porn", 128),
|
||||
GenreOption("Misc", 1)
|
||||
)), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("f_" + genreId, if(state) "1" else "0")
|
||||
val bits = state.fold(0) { acc, genre ->
|
||||
if(!genre.state) acc + genre.genreId else acc
|
||||
}
|
||||
builder.appendQueryParameter("f_cats", bits.toString())
|
||||
}
|
||||
}
|
||||
class GenreGroup : UriGroup<GenreOption>("Genres", listOf(
|
||||
GenreOption("Dōjinshi", "doujinshi"),
|
||||
GenreOption("Manga", "manga"),
|
||||
GenreOption("Artist CG", "artistcg"),
|
||||
GenreOption("Game CG", "gamecg"),
|
||||
GenreOption("Western", "western"),
|
||||
GenreOption("Non-H", "non-h"),
|
||||
GenreOption("Image Set", "imageset"),
|
||||
GenreOption("Cosplay", "cosplay"),
|
||||
GenreOption("Asian Porn", "asianporn"),
|
||||
GenreOption("Misc", "misc")
|
||||
))
|
||||
|
||||
class AdvancedOption(name: String, val param: String, defValue: Boolean = false): Filter.CheckBox(name, defValue), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
@@ -486,13 +523,23 @@ class EHentai(override val id: Long,
|
||||
else
|
||||
"E-Hentai"
|
||||
|
||||
override fun queryAll() = ExGalleryMetadata.EmptyQuery()
|
||||
override fun queryFromUrl(url: String) = ExGalleryMetadata.UrlQuery(url, exh)
|
||||
|
||||
companion object {
|
||||
val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
val TR_SUFFIX = "TR"
|
||||
val REVERSE_PARAM = "TEH_REVERSE"
|
||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
private const val TR_SUFFIX = "TR"
|
||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||
|
||||
private val FAVORITES_BORDER_HEX_COLORS = listOf(
|
||||
"000",
|
||||
"f00",
|
||||
"fa0",
|
||||
"dd0",
|
||||
"080",
|
||||
"9f4",
|
||||
"4bf",
|
||||
"00f",
|
||||
"508",
|
||||
"e8e"
|
||||
)
|
||||
|
||||
fun buildCookies(cookies: Map<String, String>)
|
||||
= cookies.entries.joinToString(separator = "; ") {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -14,55 +13,93 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.models.NHentaiMetadata
|
||||
import exh.metadata.models.PageImageType
|
||||
import exh.metadata.models.Tag
|
||||
import exh.util.*
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* NHentai source
|
||||
*/
|
||||
|
||||
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiMetadata, JsonObject> {
|
||||
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response> {
|
||||
override val metaClass = NHentaiSearchMetadata::class
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
//TODO There is currently no way to get the most popular mangas
|
||||
//TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
|
||||
return fetchLatestUpdates(page)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
TODO("Currently unavailable!")
|
||||
}
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
TODO("Currently unavailable!")
|
||||
}
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
//Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query, {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
})
|
||||
urlImportFetchSearchManga(query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
} .map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
|
||||
val uri = if(query.isNotBlank()) {
|
||||
Uri.parse("$baseUrl/search/").buildUpon().apply {
|
||||
appendQueryParameter("q", query)
|
||||
}
|
||||
} else {
|
||||
Uri.parse(baseUrl).buildUpon()
|
||||
}
|
||||
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()?.state
|
||||
?: defaultSortFilterSelection()
|
||||
|
||||
if(sortFilter.index == 1) {
|
||||
if(query.isBlank()) error("You must specify a search query if you wish to sort by popularity!")
|
||||
uri.appendQueryParameter("sort", "popular")
|
||||
}
|
||||
|
||||
if(sortFilter.ascending) {
|
||||
return client.newCall(nhGet(uri.toString()))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val doc = it.asJsoup()
|
||||
|
||||
val lastPage = doc.selectFirst(".last")
|
||||
?.attr("href")
|
||||
?.substringAfterLast('=')
|
||||
?.toIntOrNull() ?: 1
|
||||
|
||||
val thisPage = lastPage - (page - 1)
|
||||
|
||||
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
|
||||
uri.appendQueryParameter("page", thisPage.toString())
|
||||
|
||||
nhGet(uri.toString(), page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
//Currently we have no filters
|
||||
//TODO Filter builder
|
||||
val uri = Uri.parse("$baseUrl/api/galleries/search").buildUpon()
|
||||
uri.appendQueryParameter("query", query)
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
return nhGet(uri.toString(), page)
|
||||
|
||||
return Observable.just(nhGet(uri.toString(), page))
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
||||
= throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaParse(response: Response)
|
||||
= parseResultPage(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val uri = Uri.parse("$baseUrl/api/galleries/all").buildUpon()
|
||||
val uri = Uri.parse(baseUrl).buildUpon()
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
return nhGet(uri.toString(), page)
|
||||
}
|
||||
@@ -70,124 +107,122 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiMetadata, Json
|
||||
override fun latestUpdatesParse(response: Response)
|
||||
= parseResultPage(response)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val obj = jsonParser.parse(response.body()!!.string()).asJsonObject
|
||||
return parseToManga(NHentaiMetadata.Query(obj["id"].long), obj)
|
||||
}
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
//Used so we can use a different URL for fetching manga details and opening the details in the browser
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(urlToDetailsRequest(manga.url))
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
.flatMap {
|
||||
parseToManga(manga, it).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga)
|
||||
= nhGet(manga.url)
|
||||
= nhGet(baseUrl + manga.url)
|
||||
|
||||
fun urlToDetailsRequest(url: String)
|
||||
= nhGet(baseUrl + "/api/gallery/" + url.split("/").last { it.isNotBlank() })
|
||||
|
||||
fun parseResultPage(response: Response): MangasPage {
|
||||
val res = jsonParser.parse(response.body()!!.string()).asJsonObject
|
||||
val doc = response.asJsoup()
|
||||
|
||||
val error = res.get("error")
|
||||
if(error == null) {
|
||||
val results = res.getAsJsonArray("result")?.map {
|
||||
val obj = it.asJsonObject
|
||||
parseToManga(NHentaiMetadata.Query(obj["id"].long), obj)
|
||||
// TODO Parse lang + tags
|
||||
|
||||
val mangas = doc.select(".gallery > a").map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.selectFirst(".caption").text()
|
||||
|
||||
// last() is a hack to ignore the lazy-loader placeholder image on the front page
|
||||
thumbnail_url = it.select("img").last().attr("src")
|
||||
// In some pages, the thumbnail url does not include the protocol
|
||||
if(!thumbnail_url!!.startsWith("https:")) thumbnail_url = "https:$thumbnail_url"
|
||||
}
|
||||
val numPages = res.get("num_pages")?.int
|
||||
if(results != null && numPages != null)
|
||||
return MangasPage(results, numPages > response.request().tag() as Int)
|
||||
}
|
||||
|
||||
val hasNextPage = if(!response.request().url().queryParameterNames().contains(REVERSE_PARAM)) {
|
||||
doc.selectFirst(".next") != null
|
||||
} else {
|
||||
Timber.w("An error occurred while performing the search: $error")
|
||||
response.request().url().queryParameter(REVERSE_PARAM)!!.toBoolean()
|
||||
}
|
||||
return MangasPage(emptyList(), false)
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override val metaParser: NHentaiMetadata.(JsonObject) -> Unit = { obj ->
|
||||
nhId = obj["id"].asLong
|
||||
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
|
||||
val json = GALLERY_JSON_REGEX.find(input.body()!!.string())!!.groupValues[1]
|
||||
val obj = jsonParser.parse(json).asJsonObject
|
||||
|
||||
uploadDate = obj["upload_date"].nullLong
|
||||
with(metadata) {
|
||||
nhId = obj["id"].asLong
|
||||
|
||||
favoritesCount = obj["num_favorites"].nullLong
|
||||
uploadDate = obj["upload_date"].nullLong
|
||||
|
||||
mediaId = obj["media_id"].nullString
|
||||
favoritesCount = obj["num_favorites"].nullLong
|
||||
|
||||
obj["title"].nullObj?.let { it ->
|
||||
japaneseTitle = it["japanese"].nullString
|
||||
shortTitle = it["pretty"].nullString
|
||||
englishTitle = it["english"].nullString
|
||||
}
|
||||
mediaId = obj["media_id"].nullString
|
||||
|
||||
obj["images"].nullObj?.let {
|
||||
coverImageType = it["cover"]?.get("t").nullString
|
||||
it["pages"].nullArray?.mapNotNull {
|
||||
it?.asJsonObject?.get("t").nullString
|
||||
}?.map {
|
||||
PageImageType(it)
|
||||
}?.let {
|
||||
pageImageTypes.clear()
|
||||
pageImageTypes.addAll(it)
|
||||
obj["title"].nullObj?.let { title ->
|
||||
japaneseTitle = title["japanese"].nullString
|
||||
shortTitle = title["pretty"].nullString
|
||||
englishTitle = title["english"].nullString
|
||||
}
|
||||
thumbnailImageType = it["thumbnail"]?.get("t").nullString
|
||||
}
|
||||
|
||||
scanlator = obj["scanlator"].nullString
|
||||
|
||||
obj["tags"]?.asJsonArray?.map {
|
||||
val asObj = it.asJsonObject
|
||||
Pair(asObj["type"].nullString, asObj["name"].nullString)
|
||||
}?.apply {
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
if(it.first != null && it.second != null)
|
||||
tags.add(Tag(it.first!!, it.second!!, false))
|
||||
}
|
||||
}
|
||||
|
||||
fun lazyLoadMetadata(url: String) =
|
||||
defRealm { realm ->
|
||||
val meta = NHentaiMetadata.UrlQuery(url).query(realm).findFirst()
|
||||
if(meta == null) {
|
||||
client.newCall(urlToDetailsRequest(url))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
realmTrans { realm ->
|
||||
realm.copyFromRealm(realm.createUUIDObj(queryAll().clazz.java).apply {
|
||||
metaParser(this,
|
||||
jsonParser.parse(it.body()!!.string()).asJsonObject)
|
||||
})
|
||||
}
|
||||
}
|
||||
.first()
|
||||
} else {
|
||||
Observable.just(realm.copyFromRealm(meta))
|
||||
obj["images"].nullObj?.let {
|
||||
coverImageType = it["cover"]?.get("t").nullString
|
||||
it["pages"].nullArray?.mapNotNull {
|
||||
it?.asJsonObject?.get("t").nullString
|
||||
}?.let {
|
||||
pageImageTypes = it
|
||||
}
|
||||
thumbnailImageType = it["thumbnail"]?.get("t").nullString
|
||||
}
|
||||
|
||||
scanlator = obj["scanlator"].nullString
|
||||
|
||||
obj["tags"]?.asJsonArray?.map {
|
||||
val asObj = it.asJsonObject
|
||||
Pair(asObj["type"].nullString, asObj["name"].nullString)
|
||||
}?.apply {
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
if (it.first != null && it.second != null)
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) {
|
||||
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga)
|
||||
= lazyLoadMetadata(manga.url).map {
|
||||
listOf(SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
date_upload = ((it.uploadDate ?: 0) * 1000)
|
||||
chapter_number = 1f
|
||||
})
|
||||
}!!
|
||||
= Observable.just(listOf(SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}))
|
||||
|
||||
override fun fetchPageList(chapter: SChapter)
|
||||
= lazyLoadMetadata(chapter.url).map { metadata ->
|
||||
= getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
|
||||
if(metadata.mediaId == null) emptyList()
|
||||
else
|
||||
metadata.pageImageTypes.mapIndexed { index, s ->
|
||||
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s.type!!)
|
||||
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
|
||||
Page(index, imageUrl!!, imageUrl)
|
||||
}
|
||||
}!!
|
||||
}.toObservable()
|
||||
|
||||
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
|
||||
|
||||
@@ -207,6 +242,14 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiMetadata, Json
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(SortFilter())
|
||||
|
||||
class SortFilter : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("Date", "Popular"),
|
||||
defaultSortFilterSelection()
|
||||
)
|
||||
|
||||
val appName by lazy {
|
||||
context.getString(R.string.app_name)!!
|
||||
}
|
||||
@@ -226,14 +269,16 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiMetadata, Json
|
||||
|
||||
override val name = "nhentai"
|
||||
|
||||
override val baseUrl = NHentaiMetadata.BASE_URL
|
||||
override val baseUrl = NHentaiSearchMetadata.BASE_URL
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun queryAll() = NHentaiMetadata.EmptyQuery()
|
||||
override fun queryFromUrl(url: String) = NHentaiMetadata.UrlQuery(url)
|
||||
|
||||
companion object {
|
||||
private val GALLERY_JSON_REGEX = Regex("new N.gallery\\((.*)\\);")
|
||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||
|
||||
private fun defaultSortFilterSelection() = Filter.Sort.Selection(0, false)
|
||||
|
||||
val jsonParser by lazy {
|
||||
JsonParser()
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.ChapterRecognition
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.EMULATED_TAG_NAMESPACE
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.models.PervEdenGalleryMetadata
|
||||
import exh.metadata.models.PervEdenLang
|
||||
import exh.metadata.models.PervEdenTitle
|
||||
import exh.metadata.models.Tag
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
@@ -20,11 +22,17 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
// TODO Transform into delegated source
|
||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(),
|
||||
LewdSource<PervEdenGalleryMetadata, Document> {
|
||||
LewdSource<PervEdenSearchMetadata, Document> {
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
override val metaClass = PervEdenSearchMetadata::class
|
||||
|
||||
override val supportsLatest = true
|
||||
override val name = "Perv Eden"
|
||||
@@ -48,9 +56,9 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
|
||||
//Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query, {
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
})
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#mangaList > tbody > tr"
|
||||
|
||||
@@ -79,7 +87,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
val titleElement = header.child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text().trim()
|
||||
manga.thumbnail_url = "http:" + titleElement.getElementsByClass("mangaImage").first().attr("tmpsrc")
|
||||
manga.thumbnail_url = "https:" + header.parent().selectFirst(".mangaImage img").attr("tmpsrc")
|
||||
return manga
|
||||
}
|
||||
|
||||
@@ -107,67 +115,90 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override val metaParser: PervEdenGalleryMetadata.(Document) -> Unit = { document ->
|
||||
url = Uri.parse(document.location()).path
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pvId = PervEdenGalleryMetadata.pvIdFromUrl(url!!)
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = Uri.parse(input.location()).path
|
||||
|
||||
lang = this@PervEden.lang
|
||||
pvId = PervEdenGalleryMetadata.pvIdFromUrl(url!!)
|
||||
|
||||
title = document.getElementsByClass("manga-title").first()?.text()
|
||||
lang = this@PervEden.lang
|
||||
|
||||
thumbnailUrl = "http:" + document.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src")
|
||||
title = input.getElementsByClass("manga-title").first()?.text()
|
||||
|
||||
val rightBoxElement = document.select(".rightBox:not(.info)").first()
|
||||
thumbnailUrl = "http:" + input.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src")
|
||||
|
||||
altTitles.clear()
|
||||
tags.clear()
|
||||
var inStatus: String? = null
|
||||
rightBoxElement.childNodes().forEach {
|
||||
if(it is Element && it.tagName().toLowerCase() == "h4") {
|
||||
inStatus = it.text().trim()
|
||||
} else {
|
||||
when(inStatus) {
|
||||
"Alternative name(s)" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
altTitles.add(PervEdenTitle(this, text))
|
||||
val rightBoxElement = input.select(".rightBox:not(.info)").first()
|
||||
|
||||
val newAltTitles = mutableListOf<String>()
|
||||
tags.clear()
|
||||
var inStatus: String? = null
|
||||
rightBoxElement.childNodes().forEach {
|
||||
if(it is Element && it.tagName().toLowerCase() == "h4") {
|
||||
inStatus = it.text().trim()
|
||||
} else {
|
||||
when(inStatus) {
|
||||
"Alternative name(s)" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
newAltTitles += text
|
||||
}
|
||||
}
|
||||
}
|
||||
"Artist" -> {
|
||||
if(it is Element && it.tagName() == "a") {
|
||||
artist = it.text()
|
||||
tags.add(Tag("artist", it.text().toLowerCase(), false))
|
||||
"Artist" -> {
|
||||
if(it is Element && it.tagName() == "a") {
|
||||
artist = it.text()
|
||||
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
"Genres" -> {
|
||||
if(it is Element && it.tagName() == "a")
|
||||
tags.add(Tag(EMULATED_TAG_NAMESPACE, it.text().toLowerCase(), false))
|
||||
}
|
||||
"Type" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
type = text
|
||||
"Genres" -> {
|
||||
if(it is Element && it.tagName() == "a")
|
||||
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT)
|
||||
}
|
||||
}
|
||||
"Status" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
status = text
|
||||
"Type" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
type = text
|
||||
}
|
||||
}
|
||||
"Status" -> {
|
||||
if(it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if(!text.isBlank())
|
||||
status = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rating = document.getElementById("rating-score")?.attr("value")?.toFloat()
|
||||
altTitles = newAltTitles
|
||||
|
||||
rating = input.getElementById("rating-score")?.attr("value")?.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga
|
||||
= parseToManga(queryFromUrl(document.location()), document)
|
||||
= throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val num = when (lang) {
|
||||
@@ -206,9 +237,6 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
override fun imageUrlParse(document: Document)
|
||||
= "http:" + document.getElementById("mainImg").attr("src")!!
|
||||
|
||||
override fun queryAll() = PervEdenGalleryMetadata.EmptyQuery()
|
||||
override fun queryFromUrl(url: String) = PervEdenGalleryMetadata.UrlQuery(url, PervEdenLang.source(id))
|
||||
|
||||
override fun getFilterList() = FilterList (
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
|
||||
@@ -1,204 +1,91 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HENTAI_CAFE_SOURCE_ID
|
||||
import exh.metadata.EMULATED_TAG_NAMESPACE
|
||||
import exh.metadata.models.HentaiCafeMetadata
|
||||
import exh.metadata.models.HentaiCafeMetadata.Companion.BASE_URL
|
||||
import exh.metadata.models.Tag
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.Request
|
||||
import okhttp3.HttpUrl
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class HentaiCafe : ParsedHttpSource(), LewdSource<HentaiCafeMetadata, Document> {
|
||||
override val id = HENTAI_CAFE_SOURCE_ID
|
||||
|
||||
class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document> {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
override val lang = "en"
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
override val metaClass = HentaiCafeSearchMetadata::class
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun queryAll() = HentaiCafeMetadata.EmptyQuery()
|
||||
override fun queryFromUrl(url: String) = HentaiCafeMetadata.UrlQuery(url)
|
||||
|
||||
override val name = "Hentai Cafe"
|
||||
override val baseUrl = "https://hentai.cafe"
|
||||
|
||||
// Defer popular manga -> latest updates
|
||||
override fun popularMangaSelector() = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun fetchPopularManga(page: Int) = fetchLatestUpdates(page)
|
||||
|
||||
//Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query, {
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
})
|
||||
override fun searchMangaSelector() = "article.post:not(#post-0)"
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val thumb = element.select(".entry-thumb > img")
|
||||
val title = element.select(".entry-title > a")
|
||||
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(title.attr("href"))
|
||||
this.title = title.text()
|
||||
|
||||
thumbnail_url = thumb.attr("src")
|
||||
}
|
||||
}
|
||||
override fun searchMangaNextPageSelector() = ".x-pagination > ul > li:last-child > a.prev-next"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if(query.isNotBlank()) {
|
||||
//Filter by query
|
||||
"$baseUrl/page/$page/?s=${Uri.encode(query)}"
|
||||
} else if(filters.filterIsInstance<ShowBooksOnlyFilter>().any { it.state }) {
|
||||
//Filter by book
|
||||
"$baseUrl/category/book/page/$page/"
|
||||
} else {
|
||||
//Filter by tag
|
||||
val tagFilter = filters.filterIsInstance<TagFilter>().first()
|
||||
|
||||
if(tagFilter.state == 0) throw IllegalArgumentException("No filters active, no query active! What to filter?")
|
||||
|
||||
val tag = tagFilter.values[tagFilter.state]
|
||||
"$baseUrl/tag/${tag.id}/page/$page/"
|
||||
}
|
||||
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$BASE_URL/page/$page/")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return parseToManga(queryFromUrl(document.location()), document)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return lazyLoadMeta(queryFromUrl(manga.url),
|
||||
client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { it.asJsoup() }
|
||||
).map {
|
||||
listOf(SChapter.create().apply {
|
||||
url = "/manga/read/${it.readerId}/en/0/1/"
|
||||
|
||||
name = "Chapter"
|
||||
|
||||
chapter_number = 1f
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pageItems = document.select(".dropdown > li > a")
|
||||
|
||||
return pageItems.mapIndexed { index, element ->
|
||||
Page(index, element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document)
|
||||
= document.select("#page img").attr("src")
|
||||
|
||||
override val metaParser: HentaiCafeMetadata.(Document) -> Unit = {
|
||||
val content = it.getElementsByClass("content")
|
||||
val eTitle = content.select("h3")
|
||||
|
||||
url = Uri.decode(it.location())
|
||||
title = eTitle.text()
|
||||
|
||||
thumbnailUrl = content.select("img").attr("src")
|
||||
|
||||
tags.clear()
|
||||
val eDetails = content.select("p > a[rel=tag]")
|
||||
eDetails.forEach {
|
||||
val href = it.attr("href")
|
||||
val parsed = Uri.parse(href)
|
||||
val firstPath = parsed.pathSegments.first()
|
||||
|
||||
when(firstPath) {
|
||||
"tag" -> tags.add(Tag(EMULATED_TAG_NAMESPACE, it.text(), false))
|
||||
"artist" -> {
|
||||
artist = it.text()
|
||||
tags.add(Tag("artist", it.text(), false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: HentaiCafeSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = input.location()
|
||||
title = input.select(".entry-title").text()
|
||||
val contentElement = input.select(".entry-content").first()
|
||||
thumbnailUrl = contentElement.child(0).child(0).attr("src")
|
||||
|
||||
fun filterableTagsOfType(type: String) = contentElement.select("a")
|
||||
.filter { "$baseUrl/$type/" in it.attr("href") }
|
||||
.map { it.text() }
|
||||
|
||||
tags.clear()
|
||||
tags += filterableTagsOfType("tag").map {
|
||||
RaisedTag(null, it, TAG_TYPE_DEFAULT)
|
||||
}
|
||||
|
||||
val artists = filterableTagsOfType("artist")
|
||||
|
||||
artist = artists.joinToString()
|
||||
tags += artists.map {
|
||||
RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
|
||||
readerId = HttpUrl.parse(input.select("[title=Read]").attr("href"))!!.pathSegments()[2]
|
||||
}
|
||||
|
||||
readerId = Uri.parse(content.select("a[title=Read]").attr("href")).pathSegments[2]
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TagFilter(),
|
||||
ShowBooksOnlyFilter()
|
||||
)
|
||||
|
||||
class ShowBooksOnlyFilter : Filter.CheckBox("Show books only")
|
||||
|
||||
class TagFilter : Filter.Select<HCTag>("Filter by tag", listOf(
|
||||
"???" to "None",
|
||||
|
||||
"ahegao" to "Ahegao",
|
||||
"anal" to "Anal",
|
||||
"big-ass" to "Big ass",
|
||||
"big-breast" to "Big Breast",
|
||||
"bondage" to "Bondage",
|
||||
"cheating" to "Cheating",
|
||||
"chubby" to "Chubby",
|
||||
"condom" to "Condom",
|
||||
"cosplay" to "Cosplay",
|
||||
"cunnilingus" to "Cunnilingus",
|
||||
"dark-skin" to "Dark skin",
|
||||
"defloration" to "Defloration",
|
||||
"exhibitionism" to "Exhibitionism",
|
||||
"fellatio" to "Fellatio",
|
||||
"femdom" to "Femdom",
|
||||
"flat-chest" to "Flat chest",
|
||||
"full-color" to "Full color",
|
||||
"glasses" to "Glasses",
|
||||
"group" to "Group",
|
||||
"hairy" to "Hairy",
|
||||
"handjob" to "Handjob",
|
||||
"harem" to "Harem",
|
||||
"housewife" to "Housewife",
|
||||
"incest" to "Incest",
|
||||
"large-breast" to "Large Breast",
|
||||
"lingerie" to "Lingerie",
|
||||
"loli" to "Loli",
|
||||
"masturbation" to "Masturbation",
|
||||
"nakadashi" to "Nakadashi",
|
||||
"netorare" to "Netorare",
|
||||
"office-lady" to "Office Lady",
|
||||
"osananajimi" to "Osananajimi",
|
||||
"paizuri" to "Paizuri",
|
||||
"pettanko" to "Pettanko",
|
||||
"rape" to "Rape",
|
||||
"schoolgirl" to "Schoolgirl",
|
||||
"sex-toys" to "Sex Toys",
|
||||
"shota" to "Shota",
|
||||
"stocking" to "Stocking",
|
||||
"swimsuit" to "Swimsuit",
|
||||
"teacher" to "Teacher",
|
||||
"tsundere" to "Tsundere",
|
||||
"uncensored" to "uncensored",
|
||||
"x-ray" to "X-ray"
|
||||
).map { HCTag(it.first, it.second) }.toTypedArray()
|
||||
)
|
||||
|
||||
class HCTag(val id: String, val displayName: String) {
|
||||
override fun toString() = displayName
|
||||
}
|
||||
override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) {
|
||||
client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { it.asJsoup() }
|
||||
.toSingle()
|
||||
}.map {
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain("/manga/read/${it.readerId}/en/0/1/")
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
)
|
||||
}.toObservable()
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import eu.kanade.tachiyomi.util.toast
|
||||
import exh.TSUMINO_SOURCE_ID
|
||||
import exh.ui.captcha.CaptchaCompletionVerifier
|
||||
import exh.ui.captcha.SolveCaptchaActivity
|
||||
import exh.metadata.EMULATED_TAG_NAMESPACE
|
||||
import exh.metadata.models.Tag
|
||||
import exh.metadata.models.TsuminoMetadata
|
||||
import exh.metadata.models.TsuminoMetadata.Companion.BASE_URL
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.*
|
||||
import org.jsoup.nodes.Document
|
||||
@@ -32,7 +33,9 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<TsuminoMetadata, Document>, CaptchaCompletionVerifier {
|
||||
class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<TsuminoSearchMetadata, Document>, CaptchaCompletionVerifier {
|
||||
override val metaClass = TsuminoSearchMetadata::class
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
override val id = TSUMINO_SOURCE_ID
|
||||
@@ -41,77 +44,76 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
override val supportsLatest = true
|
||||
override val name = "Tsumino"
|
||||
|
||||
override fun queryAll() = TsuminoMetadata.EmptyQuery()
|
||||
|
||||
override fun queryFromUrl(url: String) = TsuminoMetadata.UrlQuery(url)
|
||||
|
||||
override val baseUrl = BASE_URL
|
||||
|
||||
override val metaParser: TsuminoMetadata.(Document) -> Unit = {
|
||||
url = it.location()
|
||||
tags.clear()
|
||||
|
||||
it.getElementById("Title")?.text()?.let {
|
||||
title = it.trim()
|
||||
}
|
||||
|
||||
it.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
|
||||
tags.add(Tag("artist", it, false))
|
||||
artist = it
|
||||
}
|
||||
|
||||
it.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
|
||||
tags.add(Tag("uploader", it, false))
|
||||
uploader = it
|
||||
}
|
||||
|
||||
it.getElementById("Uploaded")?.text()?.let {
|
||||
uploadDate = TM_DATE_FORMAT.parse(it.trim()).time
|
||||
}
|
||||
|
||||
it.getElementById("Pages")?.text()?.let {
|
||||
length = it.trim().toIntOrNull()
|
||||
}
|
||||
|
||||
it.getElementById("Rating")?.text()?.let {
|
||||
ratingString = it.trim()
|
||||
}
|
||||
|
||||
it.getElementById("Category")?.children()?.first()?.text()?.let {
|
||||
category = it.trim()
|
||||
tags.add(Tag("genre", it, false))
|
||||
}
|
||||
|
||||
it.getElementById("Collection")?.children()?.first()?.text()?.let {
|
||||
collection = it.trim()
|
||||
}
|
||||
|
||||
it.getElementById("Group")?.children()?.first()?.text()?.let {
|
||||
group = it.trim()
|
||||
tags.add(Tag("group", it, false))
|
||||
}
|
||||
|
||||
parody.clear()
|
||||
it.getElementById("Parody")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
parody.add(entry)
|
||||
tags.add(Tag("parody", entry, false))
|
||||
}
|
||||
|
||||
character.clear()
|
||||
it.getElementById("Character")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
character.add(entry)
|
||||
tags.add(Tag("character", entry, false))
|
||||
}
|
||||
|
||||
it.getElementById("Tag")?.children()?.let {
|
||||
tags.addAll(it.map {
|
||||
Tag(EMULATED_TAG_NAMESPACE, it.text().trim(), false)
|
||||
})
|
||||
|
||||
override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location()).toInt()
|
||||
tags.clear()
|
||||
|
||||
input.getElementById("Title")?.text()?.let {
|
||||
title = it.trim()
|
||||
}
|
||||
|
||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
|
||||
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL))
|
||||
artist = it
|
||||
}
|
||||
|
||||
input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
|
||||
uploader = it
|
||||
}
|
||||
|
||||
input.getElementById("Uploaded")?.text()?.let {
|
||||
uploadDate = TM_DATE_FORMAT.parse(it.trim()).time
|
||||
}
|
||||
|
||||
input.getElementById("Pages")?.text()?.let {
|
||||
length = it.trim().toIntOrNull()
|
||||
}
|
||||
|
||||
input.getElementById("Rating")?.text()?.let {
|
||||
ratingString = it.trim()
|
||||
}
|
||||
|
||||
input.getElementById("Category")?.children()?.first()?.text()?.let {
|
||||
category = it.trim()
|
||||
tags.add(RaisedTag("genre", it, TAG_TYPE_VIRTUAL))
|
||||
}
|
||||
|
||||
input.getElementById("Collection")?.children()?.first()?.text()?.let {
|
||||
collection = it.trim()
|
||||
}
|
||||
|
||||
input.getElementById("Group")?.children()?.first()?.text()?.let {
|
||||
group = it.trim()
|
||||
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL))
|
||||
}
|
||||
|
||||
val newParody = mutableListOf<String>()
|
||||
input.getElementById("Parody")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newParody.add(entry)
|
||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL))
|
||||
}
|
||||
parody = newParody
|
||||
|
||||
val newCharacter = mutableListOf<String>()
|
||||
input.getElementById("Character")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newCharacter.add(entry)
|
||||
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL))
|
||||
}
|
||||
character = newCharacter
|
||||
|
||||
input.getElementById("Tag")?.children()?.let {
|
||||
tags.addAll(it.map {
|
||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun genericMangaParse(response: Response): MangasPage {
|
||||
val json = jsonParser.parse(response.body()!!.string()!!).asJsonObject
|
||||
val hasNextPage = json["PageNumber"].int < json["PageCount"].int
|
||||
@@ -121,8 +123,8 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
|
||||
SManga.create().apply {
|
||||
val id = obj["Id"].long
|
||||
setUrlWithoutDomain(TsuminoMetadata.mangaUrlFromId(id.toString()))
|
||||
thumbnail_url = TsuminoMetadata.thumbUrlFromId(id.toString())
|
||||
url = TsuminoSearchMetadata.mangaUrlFromId(id.toString())
|
||||
thumbnail_url = BASE_URL + TsuminoSearchMetadata.thumbUrlFromId(id.toString())
|
||||
|
||||
title = obj["Title"].string
|
||||
}
|
||||
@@ -199,9 +201,10 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
|
||||
//Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query, {
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
})
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Append filters again, to provide fallback in case a filter is not provided
|
||||
// Since we only work with the first filter when building the result, if the filter is provided,
|
||||
@@ -229,15 +232,28 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document)
|
||||
= parseToManga(queryFromUrl(document.location()), document)
|
||||
|
||||
= throw UnsupportedOperationException("Unused method called!")
|
||||
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
|
||||
override fun fetchChapterList(manga: SManga) = lazyLoadMeta(queryFromUrl(manga.url),
|
||||
client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { it.asJsoup() }
|
||||
).map {
|
||||
override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) {
|
||||
client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { it.asJsoup() }
|
||||
.toSingle()
|
||||
}.map {
|
||||
trickTsumino(it.tmId)
|
||||
|
||||
listOf(
|
||||
@@ -250,9 +266,9 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
}
|
||||
}.toObservable()
|
||||
|
||||
fun trickTsumino(id: String?) {
|
||||
fun trickTsumino(id: Int?) {
|
||||
if(id == null) return
|
||||
|
||||
//Make one call to /Read/View (ASP session cookie)
|
||||
@@ -330,7 +346,8 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
this,
|
||||
cookiesMap,
|
||||
CAPTCHA_SCRIPT,
|
||||
"$BASE_URL/Read/Auth/$id")
|
||||
"$BASE_URL/Read/Auth/$id",
|
||||
".book-read-button")
|
||||
} catch(t: Throwable) {
|
||||
Crashlytics.logException(t)
|
||||
context.toast("Could not launch captcha-solving activity: ${t.message}")
|
||||
@@ -338,7 +355,7 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
||||
}
|
||||
}
|
||||
|
||||
override fun verify(url: String): Boolean {
|
||||
override fun verifyNoCaptcha(url: String): Boolean {
|
||||
return Uri.parse(url).pathSegments.getOrNull(1) == "View"
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,99 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
|
||||
|
||||
// EXH -->
|
||||
presenter.loadSearches()?.let {
|
||||
navView.setSavedSearches(it)
|
||||
} ?: run {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Failed to load saved searches!")
|
||||
.content("An error occurred while loading your saved searches.")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
navView.onSaveClicked = {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Save current search query?")
|
||||
.input("My search name", "") { _, searchName ->
|
||||
val oldSavedSearches = presenter.loadSearches() ?: emptyList()
|
||||
if(searchName.isNotBlank()
|
||||
&& oldSavedSearches.size < CatalogueNavigationView.MAX_SAVED_SEARCHES) {
|
||||
val newSearches = oldSavedSearches + EXHSavedSearch(
|
||||
searchName.toString().trim(),
|
||||
presenter.query,
|
||||
presenter.sourceFilters.toList()
|
||||
)
|
||||
presenter.saveSearches(newSearches)
|
||||
navView.setSavedSearches(newSearches)
|
||||
}
|
||||
}
|
||||
.positiveText("Save")
|
||||
.negativeText("Cancel")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
|
||||
navView.onSavedSearchClicked = cb@{ indexToSearch ->
|
||||
val savedSearches = presenter.loadSearches()
|
||||
|
||||
if(savedSearches == null) {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Failed to load saved searches!")
|
||||
.content("An error occurred while loading your saved searches.")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
return@cb
|
||||
}
|
||||
|
||||
val search = savedSearches[indexToSearch]
|
||||
|
||||
presenter.sourceFilters = FilterList(search.filterList)
|
||||
navView.setFilters(presenter.filterItems)
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
drawer.closeDrawer(Gravity.END)
|
||||
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
navView.onSavedSearchDeleteClicked = cb@{ indexToDelete ->
|
||||
val savedSearches = presenter.loadSearches()
|
||||
|
||||
if(savedSearches == null) {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Failed to delete saved search!")
|
||||
.content("An error occurred while deleting the search.")
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
return@cb
|
||||
}
|
||||
|
||||
val search = savedSearches[indexToDelete]
|
||||
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Delete saved search query?")
|
||||
.content("Are you sure you wish to delete your saved search query: '${search.name}'?")
|
||||
.positiveText("Cancel")
|
||||
.negativeText("Confirm")
|
||||
.onNegative { _, _ ->
|
||||
val newSearches = savedSearches.filterIndexed { index, _ ->
|
||||
index != indexToDelete
|
||||
}
|
||||
presenter.saveSearches(newSearches)
|
||||
navView.setSavedSearches(newSearches)
|
||||
}
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
navView.onSearchClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
showProgressBar()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.os.Bundle
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
@@ -9,6 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@@ -374,4 +378,23 @@ open class BrowseCataloguePresenter(
|
||||
}
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
private val mapper = jacksonObjectMapper().enableDefaultTyping()
|
||||
fun saveSearches(searches: List<EXHSavedSearch>) {
|
||||
val serialized = mapper.writeValueAsString(searches.toTypedArray())
|
||||
prefs.eh_savedSearches().set(serialized)
|
||||
}
|
||||
|
||||
fun loadSearches(): List<EXHSavedSearch>? {
|
||||
val loaded = prefs.eh_savedSearches().getOrDefault()
|
||||
return try {
|
||||
if (!loaded.isEmpty()) mapper.readValue<Array<EXHSavedSearch>>(loaded).toList()
|
||||
else emptyList()
|
||||
} catch(t: JsonProcessingException) {
|
||||
// Load failed
|
||||
Timber.e(t, "Failed to load saved searches!")
|
||||
null
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
@@ -2,14 +2,20 @@ package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.dpToPx
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
||||
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
|
||||
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||
: SimpleNavigationView(context, attrs) {
|
||||
@@ -22,13 +28,26 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
|
||||
var onResetClicked = {}
|
||||
|
||||
// EXH -->
|
||||
var onSaveClicked = {}
|
||||
// EXH <--
|
||||
|
||||
// EXH -->
|
||||
var onSavedSearchClicked: (Int) -> Unit = {}
|
||||
// EXH <--
|
||||
|
||||
// EXH -->
|
||||
var onSavedSearchDeleteClicked: (Int) -> Unit = {}
|
||||
// EXH <--
|
||||
|
||||
init {
|
||||
recycler.adapter = adapter
|
||||
recycler.setHasFixedSize(true)
|
||||
val view = inflate(R.layout.catalogue_drawer_content)
|
||||
val view = inflate(eu.kanade.tachiyomi.R.layout.catalogue_drawer_content)
|
||||
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
||||
addView(view)
|
||||
title.text = context?.getString(R.string.source_search_options)
|
||||
title.text = context?.getString(eu.kanade.tachiyomi.R.string.source_search_options)
|
||||
save_search_btn.setOnClickListener { onSaveClicked() }
|
||||
search_btn.setOnClickListener { onSearchClicked() }
|
||||
reset_btn.setOnClickListener { onResetClicked() }
|
||||
}
|
||||
@@ -37,4 +56,33 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
adapter.updateDataSet(items)
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun setSavedSearches(searches: List<EXHSavedSearch>) {
|
||||
saved_searches.removeAllViews()
|
||||
|
||||
val outValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
|
||||
|
||||
save_search_btn.visibility = if(searches.size < 5) View.VISIBLE else View.GONE
|
||||
|
||||
searches.forEachIndexed { index, search ->
|
||||
val restoreBtn = TextView(context)
|
||||
restoreBtn.text = search.name
|
||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
params.gravity = Gravity.CENTER
|
||||
restoreBtn.layoutParams = params
|
||||
restoreBtn.gravity = Gravity.CENTER
|
||||
restoreBtn.setBackgroundResource(outValue.resourceId)
|
||||
restoreBtn.setPadding(8.dpToPx, 8.dpToPx, 8.dpToPx, 8.dpToPx)
|
||||
restoreBtn.setOnClickListener { onSavedSearchClicked(index) }
|
||||
restoreBtn.setOnLongClickListener { onSavedSearchDeleteClicked(index); true }
|
||||
saved_searches.addView(restoreBtn)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_SAVED_SEARCHES = 5
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
data class EXHSavedSearch(val name: String,
|
||||
val query: String,
|
||||
val filterList: List<Filter<*>>)
|
||||
@@ -1,17 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import exh.*
|
||||
import exh.metadata.metadataClass
|
||||
import exh.metadata.models.SearchableGalleryMetadata
|
||||
import exh.metadata.syncMangaIds
|
||||
import exh.isLewdSource
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.search.SearchEngine
|
||||
import exh.util.defRealm
|
||||
import io.realm.RealmResults
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.concurrent.thread
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
@@ -21,6 +18,7 @@ import kotlin.concurrent.thread
|
||||
class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||
// --> EH
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val searchEngine = SearchEngine()
|
||||
// <-- EH
|
||||
|
||||
@@ -38,15 +36,6 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = list.toList()
|
||||
|
||||
// Sync manga IDs in background (EH)
|
||||
thread {
|
||||
//Wait 1s to reduce UI stutter during animations
|
||||
Thread.sleep(2000)
|
||||
defRealm {
|
||||
it.syncMangaIds(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
performFilter()
|
||||
}
|
||||
|
||||
@@ -61,104 +50,43 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
|
||||
|
||||
fun performFilter() {
|
||||
if(searchText.isNotBlank()) {
|
||||
if(cacheText != searchText) {
|
||||
globalSearchCache.clear()
|
||||
cacheText = searchText
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
try {
|
||||
val thisCache = globalSearchCache.getOrPut(view.category.name) {
|
||||
SearchCache(mangas.size)
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val parsedQuery = searchEngine.parseQuery(searchText)
|
||||
val sqlQuery = searchEngine.queryToSql(parsedQuery)
|
||||
val queryResult = db.lowLevel().rawQuery(RawQuery.builder()
|
||||
.query(sqlQuery.first)
|
||||
.args(*sqlQuery.second.toTypedArray())
|
||||
.build())
|
||||
|
||||
val convertedResult = ArrayList<Long>(queryResult.count)
|
||||
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
|
||||
queryResult.moveToFirst()
|
||||
while(queryResult.count > 0 && !queryResult.isAfterLast) {
|
||||
convertedResult += queryResult.getLong(mangaIdCol)
|
||||
queryResult.moveToNext()
|
||||
}
|
||||
|
||||
if(thisCache.ready) {
|
||||
//Skip everything if cache matches our query exactly
|
||||
updateDataSet(mangas.filter {
|
||||
thisCache.cache[it.manga.id] ?: false
|
||||
})
|
||||
} else {
|
||||
thisCache.cache.clear()
|
||||
|
||||
val parsedQuery = searchEngine.parseQuery(searchText)
|
||||
var totalFilteredSize = 0
|
||||
|
||||
val metadata = view.controller.meta!!.map {
|
||||
val meta: RealmResults<out SearchableGalleryMetadata> = if (it.value.isNotEmpty())
|
||||
searchEngine.filterResults(it.value.where(),
|
||||
parsedQuery,
|
||||
it.value.first()!!.titleFields)
|
||||
.sort(SearchableGalleryMetadata::mangaId.name)
|
||||
.findAll().apply {
|
||||
totalFilteredSize += size
|
||||
}
|
||||
else
|
||||
it.value
|
||||
Pair(it.key, meta)
|
||||
}.toMap()
|
||||
|
||||
val out = ArrayList<LibraryItem>(mangas.size)
|
||||
|
||||
var lewdMatches = 0
|
||||
|
||||
for(manga in mangas) {
|
||||
// --> EH
|
||||
try {
|
||||
if (isLewdSource(manga.manga.source)) {
|
||||
//Stop matching lewd manga if we have matched them all already!
|
||||
if (lewdMatches >= totalFilteredSize)
|
||||
continue
|
||||
|
||||
val metaClass = manga.manga.metadataClass
|
||||
val unfilteredMeta = view.controller.meta!![metaClass]
|
||||
val filteredMeta = metadata[metaClass]
|
||||
|
||||
val hasMeta = manga.hasMetadata ?: (unfilteredMeta
|
||||
?.where()
|
||||
?.equalTo(SearchableGalleryMetadata::mangaId.name, manga.manga.id)
|
||||
?.count() ?: 0 > 0)
|
||||
|
||||
if (hasMeta) {
|
||||
if (filteredMeta!!.where()
|
||||
.equalTo(SearchableGalleryMetadata::mangaId.name, manga.manga.id)
|
||||
.count() > 0) {
|
||||
//Metadata match!
|
||||
lewdMatches++
|
||||
thisCache.cache[manga.manga.id!!] = true
|
||||
out += manga
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Could not filter manga! %s", manga.manga)
|
||||
}
|
||||
|
||||
//Fallback to regular filter
|
||||
val filterRes = manga.filter(searchText)
|
||||
thisCache.cache[manga.manga.id!!] = filterRes
|
||||
if(filterRes) out += manga
|
||||
// <-- EH
|
||||
val out = mangas.filter {
|
||||
if(isLewdSource(it.manga.source)) {
|
||||
convertedResult.binarySearch(it.manga.id) >= 0
|
||||
} else {
|
||||
it.filter(searchText)
|
||||
}
|
||||
thisCache.ready = true
|
||||
updateDataSet(out)
|
||||
}
|
||||
|
||||
Timber.d("===> Took %s milliseconds to filter manga!", System.currentTimeMillis() - startTime)
|
||||
|
||||
updateDataSet(out)
|
||||
} catch(e: Exception) {
|
||||
Timber.w(e, "Could not filter mangas!")
|
||||
updateDataSet(mangas)
|
||||
}
|
||||
// EXH <--
|
||||
} else {
|
||||
globalSearchCache.clear()
|
||||
updateDataSet(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
class SearchCache(size: Int) {
|
||||
var ready = false
|
||||
var cache = HashMap<Long, Boolean>(size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
var cacheText: String? = null
|
||||
val globalSearchCache = ConcurrentHashMap<String, SearchCache>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
subscriptions += controller.searchRelay
|
||||
.doOnNext { adapter.searchText = it }
|
||||
.skip(1)
|
||||
.debounce(350, TimeUnit.MILLISECONDS)
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { adapter.performFilter() }
|
||||
|
||||
|
||||
@@ -38,10 +38,6 @@ import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import exh.favorites.FavoritesIntroDialog
|
||||
import exh.favorites.FavoritesSyncStatus
|
||||
import exh.metadata.loadAllMetadata
|
||||
import exh.metadata.models.SearchableGalleryMetadata
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.android.synthetic.main.library_controller.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import rx.Subscription
|
||||
@@ -51,7 +47,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
class LibraryController(
|
||||
@@ -130,10 +125,6 @@ class LibraryController(
|
||||
private var searchViewSubscription: Subscription? = null
|
||||
|
||||
// --> EH
|
||||
//Cached realm
|
||||
var realm: Realm? = null
|
||||
//Cached metadata
|
||||
var meta: Map<KClass<out SearchableGalleryMetadata>, RealmResults<out SearchableGalleryMetadata>>? = null
|
||||
//Sync dialog
|
||||
private var favSyncDialog: MaterialDialog? = null
|
||||
//Old sync status
|
||||
@@ -159,16 +150,6 @@ class LibraryController(
|
||||
return inflater.inflate(R.layout.library_controller, container, false)
|
||||
}
|
||||
|
||||
// --> EH
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||
//Load realm
|
||||
realm = Realm.getDefaultInstance()?.apply {
|
||||
meta = loadAllMetadata()
|
||||
}
|
||||
return super.onCreateView(inflater, container, savedViewState)
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
@@ -205,12 +186,6 @@ class LibraryController(
|
||||
tabsVisibilitySubscription?.unsubscribe()
|
||||
tabsVisibilitySubscription = null
|
||||
super.onDestroyView(view)
|
||||
|
||||
// --> EH
|
||||
//Clean up realm
|
||||
realm?.close()
|
||||
meta = null
|
||||
// <-- EH
|
||||
}
|
||||
|
||||
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
|
||||
|
||||
@@ -15,7 +15,6 @@ import android.support.v7.graphics.drawable.DrawerArrowDrawable
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.*
|
||||
import eu.kanade.tachiyomi.Migrations
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
@@ -29,27 +28,20 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||
import exh.metadata.loadAllMetadata
|
||||
import exh.uconfig.WarnConfigureDialogController
|
||||
import exh.ui.batchadd.BatchAddController
|
||||
import exh.ui.lock.LockChangeHandler
|
||||
import exh.ui.lock.LockController
|
||||
import exh.ui.lock.lockEnabled
|
||||
import exh.ui.lock.notifyLockSecurity
|
||||
import exh.ui.migration.MetadataFetchDialog
|
||||
import exh.util.defRealm
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||
import eu.kanade.tachiyomi.util.vibrate
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import rx.schedulers.Schedulers
|
||||
import exh.EXHMigrations
|
||||
import exh.ui.migration.MetadataFetchDialog
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
@@ -176,35 +168,32 @@ class MainActivity : BaseActivity() {
|
||||
notifyLockSecurity(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Early hitomi.la refresh
|
||||
if(preferences.eh_hl_earlyRefresh().getOrDefault()) {
|
||||
(Injekt.get<SourceManager>().get(HITOMI_SOURCE_ID) as Hitomi)
|
||||
.ensureCacheLoaded(false)
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.subscribe()
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
syncActivityViewWithController(router.backstack.lastOrNull()?.controller())
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Show changelog if needed
|
||||
if (Migrations.upgrade(preferences)) {
|
||||
// TODO
|
||||
// if (Migrations.upgrade(preferences)) {
|
||||
// ChangelogDialogController().showDialog(router)
|
||||
// }
|
||||
|
||||
// EXH -->
|
||||
// Perform EXH specific migrations
|
||||
if(EXHMigrations.upgrade(preferences)) {
|
||||
ChangelogDialogController().showDialog(router)
|
||||
}
|
||||
|
||||
// Migrate metadata if empty (EH)
|
||||
if(!defRealm {
|
||||
it.loadAllMetadata().any {
|
||||
it.value.isNotEmpty()
|
||||
}
|
||||
}) MetadataFetchDialog().askMigration(this, false)
|
||||
if(!preferences.migrateLibraryAsked().getOrDefault()) {
|
||||
MetadataFetchDialog().askMigration(this, false)
|
||||
}
|
||||
|
||||
// Upload settings
|
||||
if(preferences.enableExhentai().getOrDefault()
|
||||
&& preferences.eh_showSettingsUploadWarning().getOrDefault())
|
||||
WarnConfigureDialogController.uploadSettings(router)
|
||||
// EXH <--
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import exh.ui.webview.WebViewActivity
|
||||
import jp.wasabeef.glide.transformations.CropSquareTransformation
|
||||
import jp.wasabeef.glide.transformations.MaskTransformation
|
||||
import kotlinx.android.synthetic.main.manga_info_controller.*
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
@@ -139,7 +140,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
var text = tag
|
||||
if(isEHentaiBasedSource()) {
|
||||
val parsed = parseTag(text)
|
||||
text = wrapTag(parsed.first, parsed.second)
|
||||
text = wrapTag(parsed.first, parsed.second.substringBefore('|').trim())
|
||||
}
|
||||
performGlobalSearch(text)
|
||||
}
|
||||
@@ -386,6 +387,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
fun onFetchMangaError(error: Throwable) {
|
||||
setRefreshing(false)
|
||||
activity?.toast(error.message)
|
||||
// EXH -->
|
||||
Timber.e(error, "Failed to fetch manga details!")
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,23 +14,29 @@ import android.view.*
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.SeekBar
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
import com.jakewharton.rxbinding.widget.checkedChanges
|
||||
import com.jakewharton.rxbinding.widget.textChanges
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.*
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
|
||||
@@ -46,6 +52,7 @@ import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
|
||||
@@ -76,6 +83,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
var menuVisible = false
|
||||
private set
|
||||
|
||||
// --> EH
|
||||
private var ehUtilsVisible = false
|
||||
|
||||
private val exhSubscriptions = CompositeSubscription()
|
||||
|
||||
private var autoscrollSubscription: Subscription? = null
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
// <-- EH
|
||||
|
||||
/**
|
||||
* System UI helper to hide status & navigation bar on all different API levels.
|
||||
*/
|
||||
@@ -132,12 +148,49 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
if (savedState != null) {
|
||||
menuVisible = savedState.getBoolean(::menuVisible.name)
|
||||
// --> EH
|
||||
ehUtilsVisible = savedState.getBoolean(::ehUtilsVisible.name)
|
||||
// <-- EH
|
||||
}
|
||||
|
||||
config = ReaderConfig()
|
||||
initializeMenu()
|
||||
}
|
||||
|
||||
// --> EH
|
||||
private fun setEhUtilsVisibility(visible: Boolean) {
|
||||
if(visible) {
|
||||
eh_utils.visible()
|
||||
expand_eh_button.setImageResource(R.drawable.ic_keyboard_arrow_up_white_32dp)
|
||||
} else {
|
||||
eh_utils.gone()
|
||||
expand_eh_button.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
|
||||
}
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
// --> EH
|
||||
private fun setupAutoscroll(interval: Float) {
|
||||
exhSubscriptions.remove(autoscrollSubscription)
|
||||
autoscrollSubscription = null
|
||||
|
||||
if(interval == -1f) return
|
||||
|
||||
val intervalMs = (interval * 1000).roundToLong()
|
||||
val sub = Observable.interval(intervalMs, intervalMs, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
viewer.let { v ->
|
||||
if(v is PagerViewer) v.moveToNext()
|
||||
else if(v is WebtoonViewer) v.scrollDown()
|
||||
}
|
||||
}
|
||||
|
||||
autoscrollSubscription = sub
|
||||
exhSubscriptions += sub
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
/**
|
||||
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
|
||||
*/
|
||||
@@ -157,6 +210,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
*/
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(::menuVisible.name, menuVisible)
|
||||
// EXH -->
|
||||
outState.putBoolean(::ehUtilsVisible.name, ehUtilsVisible)
|
||||
// EXH <--
|
||||
if (!isChangingConfigurations) {
|
||||
presenter.onSaveInstanceStateNonConfigurationChange()
|
||||
}
|
||||
@@ -257,10 +313,151 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
}
|
||||
}
|
||||
|
||||
// --> EH
|
||||
exhSubscriptions += expand_eh_button.clicks().subscribe {
|
||||
ehUtilsVisible = !ehUtilsVisible
|
||||
setEhUtilsVisibility(ehUtilsVisible)
|
||||
}
|
||||
|
||||
eh_autoscroll_freq.setText(preferences.eh_utilAutoscrollInterval().getOrDefault().let {
|
||||
if(it == -1f)
|
||||
""
|
||||
else it.toString()
|
||||
})
|
||||
|
||||
exhSubscriptions += eh_autoscroll.checkedChanges()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
setupAutoscroll(if(it)
|
||||
preferences.eh_utilAutoscrollInterval().getOrDefault()
|
||||
else -1f)
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_autoscroll_freq.textChanges()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val parsed = it?.toString()?.toFloatOrNull()
|
||||
|
||||
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
||||
eh_autoscroll_freq.error = "Invalid frequency"
|
||||
preferences.eh_utilAutoscrollInterval().set(-1f)
|
||||
eh_autoscroll.isEnabled = false
|
||||
setupAutoscroll(-1f)
|
||||
} else {
|
||||
eh_autoscroll_freq.error = null
|
||||
preferences.eh_utilAutoscrollInterval().set(parsed)
|
||||
eh_autoscroll.isEnabled = true
|
||||
setupAutoscroll(if(eh_autoscroll.isChecked) parsed else -1f)
|
||||
}
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_autoscroll_help.clicks().subscribe {
|
||||
MaterialDialog.Builder(this)
|
||||
.title("Autoscroll help")
|
||||
.content("Automatically scroll to the next page in the specified interval. Interval is specified in seconds.")
|
||||
.positiveText("Ok")
|
||||
.show()
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_retry_all.clicks().subscribe {
|
||||
var retried = 0
|
||||
|
||||
presenter.viewerChaptersRelay.value
|
||||
.currChapter
|
||||
.pages
|
||||
?.forEachIndexed { index, page ->
|
||||
var shouldQueuePage = false
|
||||
if(page.status == Page.ERROR) {
|
||||
shouldQueuePage = true
|
||||
} else if(page.status == Page.LOAD_PAGE
|
||||
|| page.status == Page.DOWNLOAD_IMAGE) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if(shouldQueuePage) {
|
||||
page.status = Page.QUEUE
|
||||
} else {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
//If we are using EHentai/ExHentai, get a new image URL
|
||||
presenter.manga?.let { m ->
|
||||
val src = sourceManager.get(m.source)
|
||||
if(src is EHentai)
|
||||
page.imageUrl = null
|
||||
}
|
||||
|
||||
val loader = page.chapter.pageLoader
|
||||
if(page.index == exh_currentPage()?.index && loader is HttpPageLoader) {
|
||||
loader.boostPage(page)
|
||||
} else {
|
||||
loader?.retryPage(page)
|
||||
}
|
||||
|
||||
retried++
|
||||
}
|
||||
|
||||
toast("Retrying $retried failed pages...")
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_retry_all_help.clicks().subscribe {
|
||||
MaterialDialog.Builder(this)
|
||||
.title("Retry all help")
|
||||
.content("Re-add all failed pages to the download queue.")
|
||||
.positiveText("Ok")
|
||||
.show()
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_boost_page.clicks().subscribe {
|
||||
viewer?.let { viewer ->
|
||||
val curPage = exh_currentPage() ?: run {
|
||||
toast("This page cannot be boosted (invalid page)!")
|
||||
return@let
|
||||
}
|
||||
|
||||
if(curPage.status == Page.ERROR) {
|
||||
toast("Page failed to load, press the retry button instead!")
|
||||
} else if(curPage.status == Page.LOAD_PAGE || curPage.status == Page.DOWNLOAD_IMAGE) {
|
||||
toast("This page is already downloading!")
|
||||
} else if(curPage.status == Page.READY) {
|
||||
toast("This page has already been downloaded!")
|
||||
} else {
|
||||
val loader = (presenter.viewerChaptersRelay.value.currChapter.pageLoader as? HttpPageLoader)
|
||||
if(loader != null) {
|
||||
loader.boostPage(curPage)
|
||||
toast("Boosted current page!")
|
||||
} else {
|
||||
toast("This page cannot be boosted (invalid page loader)!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exhSubscriptions += eh_boost_page_help.clicks().subscribe {
|
||||
MaterialDialog.Builder(this)
|
||||
.title("Boost page help")
|
||||
.content("Normally the downloader can only download a specific amount of pages at the same time. This means you can be waiting for a page to download but the downloader will not start downloading the page until it has a free download slot. Pressing 'Boost page' will force the downloader to begin downloading the current page, regardless of whether or not there is an available slot.")
|
||||
.positiveText("Ok")
|
||||
.show()
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
// Set initial visibility
|
||||
setMenuVisibility(menuVisible)
|
||||
|
||||
// --> EH
|
||||
setEhUtilsVisibility(ehUtilsVisible)
|
||||
// <-- EH
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
private fun exh_currentPage(): ReaderPage? {
|
||||
val currentPage = (((viewer as? PagerViewer)?.currentPage
|
||||
?: (viewer as? WebtoonViewer)?.currentPage) as? ReaderPage)?.index
|
||||
return currentPage?.let { presenter.viewerChaptersRelay.value.currChapter.pages?.getOrNull(it) }
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
/**
|
||||
* Sets the visibility of the menu according to [visible] and with an optional parameter to
|
||||
* [animate] the views.
|
||||
@@ -282,7 +479,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
}
|
||||
}
|
||||
})
|
||||
toolbar.startAnimation(toolbarAnimation)
|
||||
// EXH -->
|
||||
header.startAnimation(toolbarAnimation)
|
||||
// EXH <--
|
||||
|
||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
||||
reader_menu_bottom.startAnimation(bottomAnimation)
|
||||
@@ -297,7 +496,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
reader_menu.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
toolbar.startAnimation(toolbarAnimation)
|
||||
// EXH -->
|
||||
header.startAnimation(toolbarAnimation)
|
||||
// EXH <--
|
||||
|
||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
|
||||
reader_menu_bottom.startAnimation(bottomAnimation)
|
||||
|
||||
@@ -70,7 +70,7 @@ class ReaderPresenter(
|
||||
/**
|
||||
* Relay for currently active viewer chapters.
|
||||
*/
|
||||
private val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
|
||||
/* [EXH] private */ val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
|
||||
|
||||
/**
|
||||
* Relay used when loading prev/next chapter needed to lock the UI (with a dialog).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
@@ -15,6 +17,7 @@ import rx.subscriptions.CompositeSubscription
|
||||
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
|
||||
|
||||
@@ -26,6 +29,9 @@ class HttpPageLoader(
|
||||
private val source: HttpSource,
|
||||
private val chapterCache: ChapterCache = Injekt.get()
|
||||
) : PageLoader() {
|
||||
// EXH -->
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
// EXH <--
|
||||
|
||||
/**
|
||||
* A queue used to manage requests one by one while allowing priorities.
|
||||
@@ -38,17 +44,23 @@ class HttpPageLoader(
|
||||
private val subscriptions = CompositeSubscription()
|
||||
|
||||
init {
|
||||
subscriptions += Observable.defer { Observable.just(queue.take().page) }
|
||||
.filter { it.status == Page.QUEUE }
|
||||
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
||||
.repeat()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
}, { error ->
|
||||
if (error !is InterruptedException) {
|
||||
Timber.e(error)
|
||||
}
|
||||
})
|
||||
// EXH -->
|
||||
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 ->
|
||||
if (error !is InterruptedException) {
|
||||
Timber.e(error)
|
||||
}
|
||||
})
|
||||
// EXH -->
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +154,9 @@ class HttpPageLoader(
|
||||
if (page.status == Page.ERROR) {
|
||||
page.status = Page.QUEUE
|
||||
}
|
||||
// EXH -->
|
||||
if(prefs.eh_readerInstantRetry().getOrDefault()) boostPage(page)
|
||||
else // EXH <--
|
||||
queue.offer(PriorityPage(page, 2))
|
||||
}
|
||||
|
||||
@@ -223,4 +238,20 @@ class HttpPageLoader(
|
||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun boostPage(page: ReaderPage) {
|
||||
if(page.status == Page.QUEUE) {
|
||||
subscriptions += Observable.just(page)
|
||||
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
}, { error ->
|
||||
if (error !is InterruptedException) {
|
||||
Timber.e(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
private var currentPage: Any? = null
|
||||
/* [EXH] private */ var currentPage: Any? = null
|
||||
|
||||
/**
|
||||
* Viewer chapters to set when the pager enters idle mode. Otherwise, if the view was settling
|
||||
|
||||
@@ -48,7 +48,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
private var currentPage: Any? = null
|
||||
/* [EXH] private */ var currentPage: Any? = null
|
||||
|
||||
/**
|
||||
* Configuration used by this viewer, like allow taps, or crop image borders.
|
||||
@@ -200,7 +200,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
/**
|
||||
* Scrolls down by [scrollDistance].
|
||||
*/
|
||||
private fun scrollDown() {
|
||||
/* [EXH] private */ fun scrollDown() {
|
||||
recycler.smoothScrollBy(0, scrollDistance)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.setting
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import android.text.Html
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
@@ -15,12 +15,13 @@ 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.toast
|
||||
import exh.debug.SettingsDebugController
|
||||
import exh.ui.migration.MetadataFetchDialog
|
||||
import exh.util.realmTrans
|
||||
import io.realm.Realm
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
@@ -74,6 +75,8 @@ class SettingsAdvancedController : SettingsController() {
|
||||
|
||||
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) }
|
||||
}
|
||||
|
||||
// --> EXH
|
||||
preferenceCategory {
|
||||
title = "Gallery metadata"
|
||||
isPersistent = false
|
||||
@@ -98,14 +101,29 @@ class SettingsAdvancedController : SettingsController() {
|
||||
summary = "Clear all library metadata. Disables tag searching in the library"
|
||||
|
||||
onClick {
|
||||
realmTrans {
|
||||
it.deleteAll()
|
||||
db.inTransaction {
|
||||
db.deleteAllSearchMetadata().executeAsBlocking()
|
||||
db.deleteAllSearchTags().executeAsBlocking()
|
||||
db.deleteAllSearchTitle().executeAsBlocking()
|
||||
}
|
||||
|
||||
context.toast("Library metadata cleared!")
|
||||
}
|
||||
}
|
||||
}
|
||||
switchPreference {
|
||||
title = "Enable delegated sources"
|
||||
key = PreferenceKeys.eh_delegateSources
|
||||
defaultValue = true
|
||||
summary = "Apply TachiyomiEH enhancements to the following sources if they are installed: ${DELEGATED_SOURCES.values.joinToString { it.sourceName }}"
|
||||
}
|
||||
|
||||
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>")
|
||||
onClick { router.pushController(SettingsDebugController().withFadeTransaction()) }
|
||||
}
|
||||
// <-- EXH
|
||||
}
|
||||
|
||||
private fun clearChapterCache() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
@@ -180,7 +181,7 @@ class SettingsGeneralController : SettingsController() {
|
||||
}
|
||||
}
|
||||
|
||||
// --> EH
|
||||
// --> EXH
|
||||
switchPreference {
|
||||
key = Keys.eh_askCategoryOnLongPress
|
||||
title = "Long-press favorite button to specify category"
|
||||
@@ -193,6 +194,14 @@ class SettingsGeneralController : SettingsController() {
|
||||
defaultValue = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
key = Keys.eh_autoSolveCaptchas
|
||||
title = "Automatically solve captcha"
|
||||
summary = "Use HIGHLY EXPERIMENTAL automatic ReCAPTCHA solver. Will be grayed out if unsupported by your device."
|
||||
defaultValue = false
|
||||
shouldDisableView = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
key = Keys.eh_incogWebview
|
||||
title = "Incognito 'Open in browser'"
|
||||
@@ -228,7 +237,7 @@ class SettingsGeneralController : SettingsController() {
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
// <-- EH
|
||||
// <-- EXH
|
||||
}
|
||||
|
||||
class LibraryColumnsDialog : DialogController() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.support.v7.preference.PreferenceScreen
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -18,41 +17,6 @@ class SettingsHlController : SettingsController() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
title = "hitomi.la"
|
||||
|
||||
editTextPreference {
|
||||
title = "Search database refresh frequency"
|
||||
summary = "How often to get new entries for the search database in hours. Setting this frequency too high may cause high CPU usage and network usage."
|
||||
key = PreferenceKeys.eh_hl_refreshFrequency
|
||||
defaultValue = "24"
|
||||
|
||||
onChange {
|
||||
it as String
|
||||
|
||||
if((it.toLongOrNull() ?: -1) <= 0) {
|
||||
context.toast("Invalid frequency. Frequency must be a positive whole number.")
|
||||
false
|
||||
} else true
|
||||
}
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
title = "Begin refreshing search database on app launch"
|
||||
summary = "Normally the search database gets refreshed (if required) when you open the hitomi.la catalogue. If you enable this option, the database gets refreshed in the background as soon as you open the app. It will result in higher data usage but may increase hitomi.la search speeds."
|
||||
key = PreferenceKeys.eh_hl_earlyRefresh
|
||||
defaultValue = false
|
||||
}
|
||||
|
||||
preference {
|
||||
title = "Force refresh search database now"
|
||||
summary = "Delete the local copy of the hitomi.la search database and download the new database now. Hitomi.la search will not work in the ~10mins that it takes to refresh the search database"
|
||||
isPersistent = false
|
||||
|
||||
onClick {
|
||||
context.toast(if((Injekt.get<SourceManager>().get(HITOMI_SOURCE_ID) as Hitomi).forceEnsureCacheLoaded()) {
|
||||
"Refreshing database. You will NOT be notified when it is complete!"
|
||||
} else {
|
||||
"Could not begin refresh process as there is already one ongoing!"
|
||||
}, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
// TODO Thumbnail quality chooser
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ class SettingsReaderController : SettingsController() {
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
// EXH -->
|
||||
intListPreference {
|
||||
key = Keys.eh_readerThreads
|
||||
title = "Download threads"
|
||||
@@ -147,6 +148,7 @@ class SettingsReaderController : SettingsController() {
|
||||
title = "Preserve reading position on read manga"
|
||||
defaultValue = false
|
||||
}
|
||||
// EXH <--
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pager_viewer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user