Linting Fixes AZ
This commit is contained in:
@@ -26,19 +26,19 @@ const val HBROWSE_SOURCE_ID = LEWD_SOURCE_SERIES + 12
|
||||
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
|
||||
|
||||
private val DELEGATED_LEWD_SOURCES = listOf(
|
||||
HentaiCafe::class,
|
||||
Pururin::class,
|
||||
Tsumino::class
|
||||
HentaiCafe::class,
|
||||
Pururin::class,
|
||||
Tsumino::class
|
||||
)
|
||||
|
||||
val LIBRARY_UPDATE_EXCLUDED_SOURCES = listOf(
|
||||
EH_SOURCE_ID,
|
||||
EXH_SOURCE_ID,
|
||||
NHENTAI_SOURCE_ID,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
TSUMINO_SOURCE_ID,
|
||||
HITOMI_SOURCE_ID,
|
||||
PURURIN_SOURCE_ID
|
||||
EH_SOURCE_ID,
|
||||
EXH_SOURCE_ID,
|
||||
NHENTAI_SOURCE_ID,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
TSUMINO_SOURCE_ID,
|
||||
HITOMI_SOURCE_ID,
|
||||
PURURIN_SOURCE_ID
|
||||
)
|
||||
|
||||
private inline fun <reified T> delegatedSourceId(): Long {
|
||||
@@ -54,6 +54,6 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
|
||||
|
||||
// This method MUST be fast!
|
||||
fun isLewdSource(source: Long) = source in 6900..6999 ||
|
||||
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
|
||||
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID
|
||||
|
||||
@@ -45,35 +45,41 @@ object EXHMigrations {
|
||||
if (oldVersion < 1) {
|
||||
db.inTransaction {
|
||||
// Migrate HentaiCafe source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6908
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6908
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
|
||||
// Migrate nhentai URLs
|
||||
val nhentaiManga = db.db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
|
||||
.build())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
|
||||
nhentaiManga.forEach {
|
||||
it.url = getUrlWithoutDomain(it.url)
|
||||
}
|
||||
|
||||
db.db.put()
|
||||
.objects(nhentaiManga)
|
||||
// Extremely slow without the resolver :/
|
||||
.withPutResolver(MangaUrlPutResolver())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
.objects(nhentaiManga)
|
||||
// Extremely slow without the resolver :/
|
||||
.withPutResolver(MangaUrlPutResolver())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,14 +91,18 @@ object EXHMigrations {
|
||||
if (oldVersion < 8405) {
|
||||
db.inTransaction {
|
||||
// Migrate HBrowse source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
// Cancel old scheduler jobs with old ids
|
||||
@@ -101,14 +111,18 @@ object EXHMigrations {
|
||||
if (oldVersion < 8408) {
|
||||
db.inTransaction {
|
||||
// Migrate Tsumino source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6909
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6909
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 8409) {
|
||||
@@ -214,10 +228,12 @@ object EXHMigrations {
|
||||
return try {
|
||||
val uri = URI(orig)
|
||||
var out = uri.path
|
||||
if (uri.query != null)
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
if (uri.fragment != null)
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
|
||||
@@ -37,15 +37,15 @@ class GalleryAdder {
|
||||
}
|
||||
} else {
|
||||
sourceManager.getVisibleCatalogueSources()
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.find {
|
||||
try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
false
|
||||
}
|
||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.find {
|
||||
try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
false
|
||||
}
|
||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||
}
|
||||
|
||||
// Map URL to manga URL
|
||||
@@ -66,10 +66,10 @@ class GalleryAdder {
|
||||
|
||||
// Use manga in DB if possible, otherwise, make a new manga
|
||||
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
|
||||
?: Manga.create(source.id).apply {
|
||||
this.url = cleanedUrl
|
||||
title = realUrl
|
||||
}
|
||||
?: Manga.create(source.id).apply {
|
||||
this.url = cleanedUrl
|
||||
title = realUrl
|
||||
}
|
||||
|
||||
// Insert created manga if not in DB before fetching details
|
||||
// This allows us to keep the metadata when fetching details
|
||||
@@ -111,8 +111,10 @@ class GalleryAdder {
|
||||
return GalleryAddEvent.Fail.NotFound(url)
|
||||
}
|
||||
|
||||
return GalleryAddEvent.Fail.Error(url,
|
||||
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim())
|
||||
return GalleryAddEvent.Fail.Error(
|
||||
url,
|
||||
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +143,6 @@ sealed class GalleryAddEvent {
|
||||
) : Fail()
|
||||
|
||||
class NotFound(galleryUrl: String) :
|
||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ object DebugFunctions {
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
fun forceUpgradeMigration() {
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
EXHMigrations.upgrade(prefs)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@ object DebugFunctions {
|
||||
val metadataManga = db.getFavoriteMangaWithMetadata().await()
|
||||
|
||||
val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga ->
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID)
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
manga
|
||||
}.toList()
|
||||
|
||||
@@ -56,13 +57,17 @@ object DebugFunctions {
|
||||
|
||||
fun addAllMangaInDatabaseToLibrary() {
|
||||
db.inTransaction {
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_FAVORITE} = 1
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_FAVORITE} = 1
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,25 +103,29 @@ object DebugFunctions {
|
||||
|
||||
fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j ->
|
||||
"""
|
||||
{
|
||||
info: ${j.id},
|
||||
isPeriod: ${j.isPeriodic},
|
||||
isPersisted: ${j.isPersisted},
|
||||
intervalMillis: ${j.intervalMillis},
|
||||
}
|
||||
{
|
||||
info: ${j.id},
|
||||
isPeriod: ${j.isPeriodic},
|
||||
isPersisted: ${j.isPersisted},
|
||||
intervalMillis: ${j.intervalMillis},
|
||||
}
|
||||
""".trimIndent()
|
||||
}.joinToString(",\n")
|
||||
|
||||
fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()
|
||||
|
||||
private fun convertSources(from: Long, to: Long) {
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $to
|
||||
WHERE ${MangaTable.COL_SOURCE} = $from
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $to
|
||||
WHERE ${MangaTable.COL_SOURCE} = $from
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,11 @@ class SettingsDebugController : SettingsController() {
|
||||
val result = it.call(DebugFunctions)
|
||||
view.text = "Function returned result:\n\n$result"
|
||||
MaterialDialog(context)
|
||||
.customView(view = hView, scrollable = true)
|
||||
.customView(view = hView, scrollable = true)
|
||||
} catch (t: Throwable) {
|
||||
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
|
||||
MaterialDialog(context)
|
||||
.customView(view = hView, scrollable = true)
|
||||
.customView(view = hView, scrollable = true)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ class EHentaiThrottleManager(
|
||||
// Throttle requests if necessary
|
||||
val now = System.currentTimeMillis()
|
||||
val timeDiff = now - lastThrottleTime
|
||||
if (timeDiff < throttleTime)
|
||||
if (timeDiff < throttleTime) {
|
||||
Thread.sleep(throttleTime - timeDiff)
|
||||
}
|
||||
|
||||
if (throttleTime < max)
|
||||
if (throttleTime < max) {
|
||||
throttleTime += inc
|
||||
}
|
||||
|
||||
lastThrottleTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
|
||||
|
||||
class EHentaiUpdateHelper(context: Context) {
|
||||
val parentLookupTable =
|
||||
MemAutoFlushingLookupTable(
|
||||
File(context.filesDir, "exh-plt.maftable"),
|
||||
GalleryEntry.Serializer()
|
||||
)
|
||||
MemAutoFlushingLookupTable(
|
||||
File(context.filesDir, "exh-plt.maftable"),
|
||||
GalleryEntry.Serializer()
|
||||
)
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
@@ -30,22 +30,24 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
*/
|
||||
fun findAcceptedRootAndDiscardOthers(sourceId: Long, chapters: List<Chapter>): Single<Triple<ChapterChain, List<ChapterChain>, Boolean>> {
|
||||
// Find other chains
|
||||
val chainsObservable = Observable.merge(chapters.map { chapter ->
|
||||
db.getChapters(chapter.url).asRxSingle().toObservable()
|
||||
}).toList().map { allChapters ->
|
||||
val chainsObservable = Observable.merge(
|
||||
chapters.map { chapter ->
|
||||
db.getChapters(chapter.url).asRxSingle().toObservable()
|
||||
}
|
||||
).toList().map { allChapters ->
|
||||
allChapters.flatMap { innerChapters -> innerChapters.map { it.manga_id!! } }.distinct()
|
||||
}.flatMap { mangaIds ->
|
||||
Observable.merge(
|
||||
mangaIds.map { mangaId ->
|
||||
Single.zip(
|
||||
db.getManga(mangaId).asRxSingle(),
|
||||
db.getChaptersByMangaId(mangaId).asRxSingle()
|
||||
) { manga, chapters ->
|
||||
ChapterChain(manga, chapters)
|
||||
}.toObservable().filter {
|
||||
it.manga.source == sourceId
|
||||
}
|
||||
mangaIds.map { mangaId ->
|
||||
Single.zip(
|
||||
db.getManga(mangaId).asRxSingle(),
|
||||
db.getChaptersByMangaId(mangaId).asRxSingle()
|
||||
) { manga, chapters ->
|
||||
ChapterChain(manga, chapters)
|
||||
}.toObservable().filter {
|
||||
it.manga.source == sourceId
|
||||
}
|
||||
}
|
||||
)
|
||||
}.toList()
|
||||
|
||||
@@ -66,65 +68,66 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
|
||||
// Copy chain chapters to curChapters
|
||||
val newChapters = toDiscard
|
||||
.flatMap { chain ->
|
||||
val meta by lazy {
|
||||
db.getFlatMetadataForManga(chain.manga.id!!)
|
||||
.executeAsBlocking()
|
||||
?.raise<EHentaiSearchMetadata>()
|
||||
}
|
||||
|
||||
chain.chapters.map { chapter ->
|
||||
// Convert old style chapters to new style chapters if possible
|
||||
if (chapter.date_upload <= 0 &&
|
||||
meta?.datePosted != null &&
|
||||
meta?.title != null) {
|
||||
chapter.name = meta!!.title!!
|
||||
chapter.date_upload = meta!!.datePosted!!
|
||||
}
|
||||
chapter
|
||||
}
|
||||
.flatMap { chain ->
|
||||
val meta by lazy {
|
||||
db.getFlatMetadataForManga(chain.manga.id!!)
|
||||
.executeAsBlocking()
|
||||
?.raise<EHentaiSearchMetadata>()
|
||||
}
|
||||
.fold(accepted.chapters) { curChapters, chapter ->
|
||||
val existing = curChapters.find { it.url == chapter.url }
|
||||
|
||||
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read
|
||||
|
||||
if (existing != null) {
|
||||
existing.read = existing.read || chapter.read
|
||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
existing.last_page_read = newLastPageRead
|
||||
}
|
||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||
curChapters
|
||||
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
|
||||
new = true
|
||||
curChapters + ChapterImpl().apply {
|
||||
manga_id = accepted.manga.id
|
||||
url = chapter.url
|
||||
name = chapter.name
|
||||
read = chapter.read
|
||||
bookmark = chapter.bookmark
|
||||
|
||||
last_page_read = chapter.last_page_read
|
||||
if (newLastPageRead != null && last_page_read <= 0) {
|
||||
last_page_read = newLastPageRead
|
||||
}
|
||||
|
||||
date_fetch = chapter.date_fetch
|
||||
date_upload = chapter.date_upload
|
||||
}
|
||||
} else curChapters
|
||||
}
|
||||
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
|
||||
.sortedBy { it.date_upload }
|
||||
.apply {
|
||||
mapIndexed { index, chapter ->
|
||||
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
|
||||
chapter.chapter_number = index + 1f
|
||||
chapter.source_order = lastIndex - index
|
||||
chain.chapters.map { chapter ->
|
||||
// Convert old style chapters to new style chapters if possible
|
||||
if (chapter.date_upload <= 0 &&
|
||||
meta?.datePosted != null &&
|
||||
meta?.title != null
|
||||
) {
|
||||
chapter.name = meta!!.title!!
|
||||
chapter.date_upload = meta!!.datePosted!!
|
||||
}
|
||||
chapter
|
||||
}
|
||||
}
|
||||
.fold(accepted.chapters) { curChapters, chapter ->
|
||||
val existing = curChapters.find { it.url == chapter.url }
|
||||
|
||||
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read
|
||||
|
||||
if (existing != null) {
|
||||
existing.read = existing.read || chapter.read
|
||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
existing.last_page_read = newLastPageRead
|
||||
}
|
||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||
curChapters
|
||||
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
|
||||
new = true
|
||||
curChapters + ChapterImpl().apply {
|
||||
manga_id = accepted.manga.id
|
||||
url = chapter.url
|
||||
name = chapter.name
|
||||
read = chapter.read
|
||||
bookmark = chapter.bookmark
|
||||
|
||||
last_page_read = chapter.last_page_read
|
||||
if (newLastPageRead != null && last_page_read <= 0) {
|
||||
last_page_read = newLastPageRead
|
||||
}
|
||||
|
||||
date_fetch = chapter.date_fetch
|
||||
date_upload = chapter.date_upload
|
||||
}
|
||||
} else curChapters
|
||||
}
|
||||
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
|
||||
.sortedBy { it.date_upload }
|
||||
.apply {
|
||||
mapIndexed { index, chapter ->
|
||||
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
|
||||
chapter.chapter_number = index + 1f
|
||||
chapter.source_order = lastIndex - index
|
||||
}
|
||||
}
|
||||
|
||||
toDiscard.forEach { it.manga.favorite = false }
|
||||
accepted.manga.favorite = true
|
||||
@@ -165,8 +168,8 @@ data class GalleryEntry(val gId: String, val gToken: String) {
|
||||
override fun read(string: String): GalleryEntry {
|
||||
val colonIndex = string.indexOf(':')
|
||||
return GalleryEntry(
|
||||
string.substring(0, colonIndex),
|
||||
string.substring(colonIndex + 1, string.length)
|
||||
string.substring(0, colonIndex),
|
||||
string.substring(colonIndex + 1, string.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,17 +137,19 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
logger.d("Filtering manga and raising metadata...")
|
||||
val curTime = System.currentTimeMillis()
|
||||
val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga ->
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID)
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val meta = db.getFlatMetadataForManga(manga.id!!).asRxSingle().await()
|
||||
?: return@mapNotNull null
|
||||
?: return@mapNotNull null
|
||||
|
||||
val raisedMeta = meta.raise<EHentaiSearchMetadata>()
|
||||
|
||||
// Don't update galleries too frequently
|
||||
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled))
|
||||
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minBy {
|
||||
it.date_upload
|
||||
@@ -172,13 +174,15 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
break
|
||||
}
|
||||
|
||||
logger.d("Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
|
||||
index,
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration,
|
||||
modifiedThisIteration.size)
|
||||
logger.d(
|
||||
"Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
|
||||
index,
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration,
|
||||
modifiedThisIteration.size
|
||||
)
|
||||
|
||||
if (manga.id in modifiedThisIteration) {
|
||||
// We already processed this manga!
|
||||
@@ -194,32 +198,37 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
failuresThisIteration++
|
||||
|
||||
logger.e("> Network error while updating gallery!", e)
|
||||
logger.e("> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration)
|
||||
logger.e(
|
||||
"> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chapters.isEmpty()) {
|
||||
logger.e("No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration)
|
||||
logger.e(
|
||||
"No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Find accepted root and discard others
|
||||
val (acceptedRoot, discardedRoots, hasNew) =
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||
|
||||
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
||||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
||||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })
|
||||
) {
|
||||
updatedManga += acceptedRoot.manga
|
||||
}
|
||||
|
||||
@@ -229,13 +238,13 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
}
|
||||
} finally {
|
||||
prefs.eh_autoUpdateStats().set(
|
||||
gson.toJson(
|
||||
EHentaiUpdaterStats(
|
||||
startTime,
|
||||
allMeta.size,
|
||||
updatedThisIteration
|
||||
)
|
||||
gson.toJson(
|
||||
EHentaiUpdaterStats(
|
||||
startTime,
|
||||
allMeta.size,
|
||||
updatedThisIteration
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (updatedManga.isNotEmpty()) {
|
||||
@@ -247,7 +256,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
// New, current
|
||||
suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||
val source = sourceManager.get(manga.source) as? EHentai
|
||||
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
|
||||
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
|
||||
|
||||
try {
|
||||
val updatedManga = source.fetchMangaDetails(manga).toSingle().await(Schedulers.io())
|
||||
@@ -288,8 +297,10 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
|
||||
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
|
||||
return JobInfo.Builder(
|
||||
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
||||
else JOB_ID_UPDATE_BACKGROUND, componentName())
|
||||
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
||||
else JOB_ID_UPDATE_BACKGROUND,
|
||||
componentName()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.periodicBackgroundJobInfo(
|
||||
@@ -298,29 +309,32 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
requireUnmetered: Boolean
|
||||
): JobInfo {
|
||||
return baseBackgroundJobInfo(false)
|
||||
.setPeriodic(period)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(
|
||||
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
else JobInfo.NETWORK_TYPE_ANY)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setRequiresBatteryNotLow(true)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
setEstimatedNetworkBytes(15000L * UPDATES_PER_ITERATION,
|
||||
1000L * UPDATES_PER_ITERATION)
|
||||
}
|
||||
.setPeriodic(period)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(
|
||||
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
else JobInfo.NETWORK_TYPE_ANY
|
||||
)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setRequiresBatteryNotLow(true)
|
||||
}
|
||||
.setRequiresCharging(requireCharging)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
setEstimatedNetworkBytes(
|
||||
15000L * UPDATES_PER_ITERATION,
|
||||
1000L * UPDATES_PER_ITERATION
|
||||
)
|
||||
}
|
||||
}
|
||||
.setRequiresCharging(requireCharging)
|
||||
// .setRequiresDeviceIdle(true) Job never seems to run with this
|
||||
.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Context.testBackgroundJobInfo(): JobInfo {
|
||||
return baseBackgroundJobInfo(true)
|
||||
.setOverrideDeadline(1)
|
||||
.build()
|
||||
.setOverrideDeadline(1)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun launchBackgroundTest(context: Context) {
|
||||
@@ -343,9 +357,9 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
val wifiRestriction = "wifi" in restrictions
|
||||
|
||||
val jobInfo = context.periodicBackgroundJobInfo(
|
||||
interval.hours.inMilliseconds.longValue,
|
||||
acRestriction,
|
||||
wifiRestriction
|
||||
interval.hours.inMilliseconds.longValue,
|
||||
acRestriction,
|
||||
wifiRestriction
|
||||
)
|
||||
|
||||
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
|
||||
|
||||
@@ -10,15 +10,16 @@ class FavoritesIntroDialog {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
fun show(context: Context) = MaterialDialog(context)
|
||||
.title(text = "IMPORTANT FAVORITES SYNC NOTES")
|
||||
.message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSyncIntro().set(false)
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
.title(text = "IMPORTANT FAVORITES SYNC NOTES")
|
||||
.message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSyncIntro().set(false)
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
|
||||
private val FAVORITES_INTRO_TEXT = """
|
||||
private val FAVORITES_INTRO_TEXT =
|
||||
"""
|
||||
1. Changes to category names in the app are <b>NOT</b> synced! Please <i>change the category names on ExHentai instead</i>. The category names will be copied from the ExHentai servers every sync.
|
||||
<br><br>
|
||||
2. The favorite categories on ExHentai correspond to the <b>first 10 categories in the app</b> (excluding the 'Default' category). <i>Galleries in other categories will <b>NOT</b> be synced!</i>
|
||||
@@ -30,5 +31,5 @@ class FavoritesIntroDialog {
|
||||
5. <b>Do NOT put favorites in multiple categories</b> (the app supports this). This can confuse the sync algorithm as ExHentai only allows each favorite to be in one category.
|
||||
<br><br>
|
||||
This dialog will only popup once. You can read these notes again by going to 'Settings > E-Hentai > Show favorites sync notes'.
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
private val exh by lazy {
|
||||
Injekt.get<SourceManager>().get(EXH_SOURCE_ID) as? EHentai
|
||||
?: EHentai(0, true, context)
|
||||
?: EHentai(0, true, context)
|
||||
}
|
||||
|
||||
private val storage = LocalFavoritesStorage()
|
||||
@@ -82,8 +82,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
if (it.id in seenManga) {
|
||||
val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
|
||||
status.onNext(FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(it, inCategories))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(it, inCategories)
|
||||
)
|
||||
logger.w("Manga %s is in multiple categories!", it.id)
|
||||
return
|
||||
} else {
|
||||
@@ -107,13 +109,17 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Take wake + wifi locks
|
||||
ignore { wakeLock?.release() }
|
||||
wakeLock = ignore {
|
||||
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"teh:ExhFavoritesSyncWakelock")
|
||||
context.powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"teh:ExhFavoritesSyncWakelock"
|
||||
)
|
||||
}
|
||||
ignore { wifiLock?.release() }
|
||||
wifiLock = ignore {
|
||||
context.wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL,
|
||||
"teh:ExhFavoritesSyncWifi")
|
||||
context.wifiManager.createWifiLock(
|
||||
WifiManager.WIFI_MODE_FULL,
|
||||
"teh:ExhFavoritesSyncWifi"
|
||||
)
|
||||
}
|
||||
|
||||
// Do not update galleries while syncing favorites
|
||||
@@ -137,8 +143,9 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
// Apply change sets
|
||||
applyChangeSetToLocal(errorList, remoteChanges)
|
||||
if (localChanges != null)
|
||||
if (localChanges != null) {
|
||||
applyChangeSetToRemote(errorList, localChanges)
|
||||
}
|
||||
|
||||
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
|
||||
storage.snapshotEntries(realm)
|
||||
@@ -173,10 +180,11 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
EHentaiUpdateWorker.scheduleBackground(context)
|
||||
}
|
||||
|
||||
if (errorList.isEmpty())
|
||||
if (errorList.isEmpty()) {
|
||||
status.onNext(FavoritesSyncStatus.Idle())
|
||||
else
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyRemoteCategories(errorList: MutableList<String>, categories: List<String>) {
|
||||
@@ -217,22 +225,25 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
// Only insert categories if changed
|
||||
if (changed)
|
||||
if (changed) {
|
||||
db.insertCategories(newLocalCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) {
|
||||
val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(FormBody.Builder()
|
||||
.add("favcat", gallery.category.toString())
|
||||
.add("favnote", "")
|
||||
.add("apply", "Add to Favorites")
|
||||
.add("update", "1")
|
||||
.build())
|
||||
.build()
|
||||
.url(url)
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("favcat", gallery.category.toString())
|
||||
.add("favnote", "")
|
||||
.add("apply", "Add to Favorites")
|
||||
.add("update", "1")
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
||||
@@ -271,8 +282,8 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
|
||||
|
||||
val formBody = FormBody.Builder()
|
||||
.add("ddact", "delete")
|
||||
.add("apply", "Apply")
|
||||
.add("ddact", "delete")
|
||||
.add("apply", "Apply")
|
||||
|
||||
// Add change set to form
|
||||
changeSet.removed.forEach {
|
||||
@@ -280,9 +291,9 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://exhentai.org/favorites.php")
|
||||
.post(formBody.build())
|
||||
.build()
|
||||
.url("https://exhentai.org/favorites.php")
|
||||
.post(formBody.build())
|
||||
.build()
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to delete galleries from the remote servers!"
|
||||
@@ -299,8 +310,12 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
||||
needWarnThrottle()))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.Processing(
|
||||
"Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
||||
needWarnThrottle()
|
||||
)
|
||||
)
|
||||
|
||||
throttleManager.throttle()
|
||||
|
||||
@@ -317,8 +332,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
val url = it.getUrl()
|
||||
|
||||
// Consider both EX and EH sources
|
||||
listOf(db.getManga(url, EXH_SOURCE_ID),
|
||||
db.getManga(url, EH_SOURCE_ID)).forEach {
|
||||
listOf(
|
||||
db.getManga(url, EXH_SOURCE_ID),
|
||||
db.getManga(url, EH_SOURCE_ID)
|
||||
).forEach {
|
||||
val manga = it.executeAsBlocking()
|
||||
|
||||
if (manga?.favorite == true) {
|
||||
@@ -340,16 +357,22 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
||||
needWarnThrottle()))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.Processing(
|
||||
"Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
||||
needWarnThrottle()
|
||||
)
|
||||
)
|
||||
|
||||
throttleManager.throttle()
|
||||
|
||||
// Import using gallery adder
|
||||
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
|
||||
true,
|
||||
exh,
|
||||
throttleManager::throttle)
|
||||
val result = galleryAdder.addGallery(
|
||||
"${exh.baseUrl}${it.getUrl()}",
|
||||
true,
|
||||
exh,
|
||||
throttleManager::throttle
|
||||
)
|
||||
|
||||
if (result is GalleryAddEvent.Fail) {
|
||||
if (result is GalleryAddEvent.Fail.NotFound) {
|
||||
@@ -370,8 +393,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
throw IgnoredException()
|
||||
}
|
||||
} else if (result is GalleryAddEvent.Success) {
|
||||
insertedMangaCategories += MangaCategory.create(result.manga,
|
||||
categories[it.category]) to result.manga
|
||||
insertedMangaCategories += MangaCategory.create(
|
||||
result.manga,
|
||||
categories[it.category]
|
||||
) to result.manga
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,12 +404,12 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
insertedMangaCategories.chunked(10).map {
|
||||
Pair(it.map { it.first }, it.map { it.second })
|
||||
}.forEach {
|
||||
db.setMangaCategories(it.first, it.second)
|
||||
}
|
||||
db.setMangaCategories(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
fun needWarnThrottle() =
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
|
||||
class IgnoredException : RuntimeException()
|
||||
|
||||
@@ -401,12 +426,15 @@ sealed class FavoritesSyncStatus(val message: String) {
|
||||
val manga: Manga,
|
||||
val categories: List<Category>
|
||||
) :
|
||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||
}
|
||||
class Initializing : FavoritesSyncStatus("Initializing sync")
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if (isThrottle)
|
||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||
else
|
||||
message)
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(
|
||||
if (isThrottle) {
|
||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||
} else {
|
||||
message
|
||||
}
|
||||
)
|
||||
class CompleteWithErrors(messages: List<String>) : FavoritesSyncStatus(messages.joinToString("\n"))
|
||||
}
|
||||
|
||||
@@ -14,41 +14,46 @@ class LocalFavoritesStorage {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val realmConfig = RealmConfiguration.Builder()
|
||||
.name("fav-sync")
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
.name("fav-sync")
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
|
||||
fun getRealm() = Realm.getInstance(realmConfig)
|
||||
|
||||
fun getChangedDbEntries(realm: Realm) =
|
||||
getChangedEntries(realm,
|
||||
getChangedEntries(
|
||||
realm,
|
||||
parseToFavoriteEntries(
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
|
||||
getChangedEntries(realm,
|
||||
getChangedEntries(
|
||||
realm,
|
||||
parseToFavoriteEntries(
|
||||
entries.asSequence().map {
|
||||
Pair(it.fav, it.manga.apply {
|
||||
entries.asSequence().map {
|
||||
Pair(
|
||||
it.fav,
|
||||
it.manga.apply {
|
||||
favorite = true
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun snapshotEntries(realm: Realm) {
|
||||
val dbMangas = parseToFavoriteEntries(
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
)
|
||||
|
||||
// Delete old snapshot
|
||||
@@ -70,29 +75,29 @@ class LocalFavoritesStorage {
|
||||
}
|
||||
|
||||
val removed = realm.where(FavoriteEntry::class.java)
|
||||
.findAll()
|
||||
.filter {
|
||||
queryListForEntry(terminated, it) == null
|
||||
}.map {
|
||||
realm.copyFromRealm(it)
|
||||
}
|
||||
.findAll()
|
||||
.filter {
|
||||
queryListForEntry(terminated, it) == null
|
||||
}.map {
|
||||
realm.copyFromRealm(it)
|
||||
}
|
||||
|
||||
return ChangeSet(added, removed)
|
||||
}
|
||||
|
||||
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
|
||||
where(FavoriteEntry::class.java)
|
||||
where(FavoriteEntry::class.java)
|
||||
.equalTo(FavoriteEntry::gid.name, entry.gid)
|
||||
.equalTo(FavoriteEntry::token.name, entry.token)
|
||||
.equalTo(FavoriteEntry::category.name, entry.category)
|
||||
.findFirst()
|
||||
|
||||
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
|
||||
list.find {
|
||||
it.gid == entry.gid &&
|
||||
list.find {
|
||||
it.gid == entry.gid &&
|
||||
it.token == entry.token &&
|
||||
it.category == entry.category
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
@@ -100,28 +105,34 @@ class LocalFavoritesStorage {
|
||||
return manga.filter(this::validateDbManga).mapNotNull {
|
||||
val category = db.getCategoriesForManga(it).executeAsBlocking()
|
||||
|
||||
Pair(dbCategories.indexOf(category.firstOrNull()
|
||||
?: return@mapNotNull null), it)
|
||||
Pair(
|
||||
dbCategories.indexOf(
|
||||
category.firstOrNull()
|
||||
?: return@mapNotNull null
|
||||
),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
|
||||
manga.filter {
|
||||
validateDbManga(it.second)
|
||||
}.mapNotNull {
|
||||
FavoriteEntry().apply {
|
||||
title = it.second.title
|
||||
gid = EHentaiSearchMetadata.galleryId(it.second.url)
|
||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||
category = it.first
|
||||
manga.filter {
|
||||
validateDbManga(it.second)
|
||||
}.mapNotNull {
|
||||
FavoriteEntry().apply {
|
||||
title = it.second.title
|
||||
gid = EHentaiSearchMetadata.galleryId(it.second.url)
|
||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||
category = it.first
|
||||
|
||||
if (this.category > MAX_CATEGORIES)
|
||||
return@mapNotNull null
|
||||
if (this.category > MAX_CATEGORIES) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDbManga(manga: Manga) =
|
||||
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
|
||||
companion object {
|
||||
const val MAX_CATEGORIES = 9
|
||||
|
||||
@@ -71,39 +71,43 @@ class HitomiNozomi(
|
||||
}
|
||||
|
||||
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
||||
if (data == null)
|
||||
if (data == null) {
|
||||
return Single.just(emptyList())
|
||||
}
|
||||
|
||||
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
||||
val (offset, length) = data
|
||||
if (length > 100000000 || length <= 0)
|
||||
if (length > 100000000 || length <= 0) {
|
||||
return Single.just(emptyList())
|
||||
}
|
||||
|
||||
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
||||
.asObservable()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
.asObservable()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { inbuf ->
|
||||
if (inbuf.isEmpty()) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { inbuf ->
|
||||
if (inbuf.isEmpty())
|
||||
return@map emptyList<Int>()
|
||||
|
||||
val view = ByteCursor(inbuf)
|
||||
val numberOfGalleryIds = view.nextInt()
|
||||
val view = ByteCursor(inbuf)
|
||||
val numberOfGalleryIds = view.nextInt()
|
||||
|
||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||
|
||||
if (numberOfGalleryIds > 10000000 ||
|
||||
numberOfGalleryIds <= 0 ||
|
||||
inbuf.size != expectedLength) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
if (numberOfGalleryIds > 10000000 ||
|
||||
numberOfGalleryIds <= 0 ||
|
||||
inbuf.size != expectedLength
|
||||
) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
|
||||
(1..numberOfGalleryIds).map {
|
||||
view.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
(1..numberOfGalleryIds).map {
|
||||
view.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
||||
@@ -112,10 +116,11 @@ class HitomiNozomi(
|
||||
for (i in 0 until top) {
|
||||
val dv1i = dv1[i].toInt() and 0xFF
|
||||
val dv2i = dv2[i].toInt() and 0xFF
|
||||
if (dv1i < dv2i)
|
||||
if (dv1i < dv2i) {
|
||||
return -1
|
||||
else if (dv1i > dv2i)
|
||||
} else if (dv1i > dv2i) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -185,16 +190,16 @@ class HitomiNozomi(
|
||||
}
|
||||
|
||||
return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { nodedata ->
|
||||
if (nodedata.isNotEmpty()) {
|
||||
decodeNode(nodedata)
|
||||
} else null
|
||||
}.toSingle()
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { nodedata ->
|
||||
if (nodedata.isNotEmpty()) {
|
||||
decodeNode(nodedata)
|
||||
} else null
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
|
||||
@@ -203,17 +208,19 @@ class HitomiNozomi(
|
||||
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
|
||||
}
|
||||
|
||||
return client.newCall(Request.Builder()
|
||||
return client.newCall(
|
||||
Request.Builder()
|
||||
.url(nozomiAddress)
|
||||
.build())
|
||||
.asObservableSuccess()
|
||||
.map { resp ->
|
||||
val body = resp.body!!.bytes()
|
||||
val cursor = ByteCursor(body)
|
||||
(1..body.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
.build()
|
||||
)
|
||||
.asObservableSuccess()
|
||||
.map { resp ->
|
||||
val body = resp.body!!.bytes()
|
||||
val cursor = ByteCursor(body)
|
||||
(1..body.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
private fun hashTerm(query: String): HashedTerm {
|
||||
@@ -233,15 +240,18 @@ class HitomiNozomi(
|
||||
private val HASH_CHARSET = Charsets.UTF_8
|
||||
|
||||
fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request {
|
||||
return GET(url, Headers.Builder()
|
||||
return GET(
|
||||
url,
|
||||
Headers.Builder()
|
||||
.add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}")
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
||||
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
||||
.asObservableSuccess()
|
||||
.map { it.body!!.string().toLong() }
|
||||
.asObservableSuccess()
|
||||
.map { it.body!!.string().toLong() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
override fun createView(root: ViewGroup, textColor: Int, textSize: Float, textAlpha: Float): View {
|
||||
val view = LinearLayout(root.context)
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
view.setPadding(4.dpToPx, 0, 4.dpToPx, 4.dpToPx)
|
||||
val textView = TextView(view.context)
|
||||
@@ -42,15 +42,16 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
textView.alpha = textAlpha
|
||||
textView.text = HtmlCompat.fromHtml(buildInfo(), HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
textView.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
view.addView(textView)
|
||||
this.textView = textView
|
||||
return view
|
||||
}
|
||||
|
||||
fun buildInfo() = """
|
||||
fun buildInfo() =
|
||||
"""
|
||||
<font color='green'>===[ ${context.getString(R.string.app_name)} ]===</font><br>
|
||||
<b>Build type:</b> ${BuildConfig.BUILD_TYPE}<br>
|
||||
<b>Debug mode:</b> ${BuildConfig.DEBUG.asEnabledString()}<br>
|
||||
@@ -58,7 +59,7 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
<b>Commit SHA:</b> ${BuildConfig.COMMIT_SHA}<br>
|
||||
<b>Log level:</b> ${EHLogLevel.currentLogLevel.name.toLowerCase()}<br>
|
||||
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().get().asEnabledString()}
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum class EHLogLevel(val description: String) {
|
||||
|
||||
fun init(context: Context) {
|
||||
curLogLevel = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getInt(PreferenceKeys.eh_logLevel, 0)
|
||||
.getInt(PreferenceKeys.eh_logLevel, 0)
|
||||
}
|
||||
|
||||
fun shouldLog(requiredLogLevel: EHLogLevel): Boolean {
|
||||
|
||||
@@ -35,31 +35,32 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank()) {
|
||||
null
|
||||
else
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) {
|
||||
forEach { action(it.key, it.value) }
|
||||
}
|
||||
|
||||
val ONGOING_SUFFIX = arrayOf(
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}",
|
||||
"<ongoing>",
|
||||
"ongoing",
|
||||
"[incomplete]",
|
||||
"(incomplete)",
|
||||
"{incomplete}",
|
||||
"<incomplete>",
|
||||
"incomplete",
|
||||
"[wip]",
|
||||
"(wip)",
|
||||
"{wip}",
|
||||
"<wip>",
|
||||
"wip"
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}",
|
||||
"<ongoing>",
|
||||
"ongoing",
|
||||
"[incomplete]",
|
||||
"(incomplete)",
|
||||
"{incomplete}",
|
||||
"<incomplete>",
|
||||
"incomplete",
|
||||
"[wip]",
|
||||
"(wip)",
|
||||
"{wip}",
|
||||
"<wip>",
|
||||
"wip"
|
||||
)
|
||||
|
||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
|
||||
|
||||
@@ -50,10 +50,11 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
// No title bug?
|
||||
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
||||
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault()) {
|
||||
altTitle ?: title
|
||||
else
|
||||
} else {
|
||||
title
|
||||
}
|
||||
titleObj?.let { manga.title = it }
|
||||
|
||||
// Set artist (if we can find one)
|
||||
@@ -102,8 +103,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -117,24 +118,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http"))
|
||||
Uri.parse(it).pathSegments
|
||||
else
|
||||
it.split('/')
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http")) {
|
||||
Uri.parse(it).pathSegments
|
||||
} else {
|
||||
it.split('/')
|
||||
}
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun galleryId(url: String) = splitGalleryUrl(url)[1]
|
||||
|
||||
fun galleryToken(url: String) =
|
||||
splitGalleryUrl(url)[2]
|
||||
splitGalleryUrl(url)[2]
|
||||
|
||||
fun normalizeUrl(url: String) =
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
|
||||
fun idAndTokenToUrl(id: String, token: String) =
|
||||
"/g/$id/$token/?nw=always"
|
||||
"/g/$id/$token/?nw=always"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -34,8 +34,8 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -31,15 +31,15 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.status = SManga.UNKNOWN
|
||||
|
||||
val detailsDesc = "Title: $title\n" +
|
||||
"Artist: $artist\n"
|
||||
"Artist: $artist\n"
|
||||
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
manga.description = listOf(detailsDesc, tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -50,6 +50,6 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
const val BASE_URL = "https://hentai.cafe"
|
||||
|
||||
fun hcIdFromUrl(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }
|
||||
url.split("/").last { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +62,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
detailsDesc += "Language: ${it.capitalize()}\n"
|
||||
}
|
||||
|
||||
if (series.isNotEmpty())
|
||||
if (series.isNotEmpty()) {
|
||||
detailsDesc += "Series: ${series.joinToString()}\n"
|
||||
}
|
||||
|
||||
if (characters.isNotEmpty())
|
||||
if (characters.isNotEmpty()) {
|
||||
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
||||
}
|
||||
|
||||
uploadDate?.let {
|
||||
detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n"
|
||||
@@ -80,8 +82,8 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -93,9 +95,9 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
const val BASE_URL = "https://hitomi.la"
|
||||
|
||||
fun hlIdFromUrl(url: String) =
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
|
||||
fun urlFromHlId(id: String) =
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
if (mediaId != null) {
|
||||
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
|
||||
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs)
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs) {
|
||||
"cover"
|
||||
else "thumb"}.$it"
|
||||
} else {
|
||||
"thumb"
|
||||
}}.$it"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +93,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -108,14 +110,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun nhUrlToId(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
|
||||
fun nhIdToPath(id: Long) = "/g/$id/"
|
||||
}
|
||||
|
||||
@@ -41,11 +41,12 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.title = it
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
if (altTitles.isNotEmpty())
|
||||
if (altTitles.isNotEmpty()) {
|
||||
titleDesc += "Alternate Titles: \n" + altTitles
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
}
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
}
|
||||
}
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
artist?.let {
|
||||
@@ -76,8 +77,8 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -87,9 +88,9 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
url.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last()
|
||||
}
|
||||
@@ -102,7 +103,7 @@ enum class PervEdenLang(val id: Long) {
|
||||
|
||||
companion object {
|
||||
fun source(id: Long) =
|
||||
values().find { it.id == id }
|
||||
values().find { it.id == id }
|
||||
?: throw IllegalArgumentException("Unknown source ID: $id!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -65,8 +65,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -77,7 +77,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
val BASE_URL = "https://www.tsumino.com"
|
||||
|
||||
fun tmIdFromUrl(url: String) =
|
||||
Uri.parse(url).lastPathSegment
|
||||
Uri.parse(url).lastPathSegment
|
||||
|
||||
fun mangaUrlFromId(id: String) = "/Book/Info/$id"
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ data class FlatMetadata(
|
||||
|
||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
|
||||
RaisedSearchMetadata.raiseFlattenGson
|
||||
.fromJson(metadata.extra, clazz.java).apply {
|
||||
fillBaseFields(this@FlatMetadata)
|
||||
}
|
||||
.fromJson(metadata.extra, clazz.java).apply {
|
||||
fillBaseFields(this@FlatMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> {
|
||||
|
||||
@@ -36,29 +36,29 @@ abstract class RaisedSearchMetadata {
|
||||
abstract fun copyTo(manga: SManga)
|
||||
|
||||
fun tagsToGenreString() =
|
||||
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
|
||||
fun tagsToDescription() =
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
|
||||
groupedTags.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
groupedTags.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
}
|
||||
this += joinedTags
|
||||
this += "\n"
|
||||
}
|
||||
this += joinedTags
|
||||
this += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun List<RaisedTag>.ofNamespace(ns: String): List<RaisedTag> {
|
||||
return filter { it.namespace == ns }
|
||||
@@ -76,23 +76,23 @@ abstract class RaisedSearchMetadata {
|
||||
indexedExtra,
|
||||
0
|
||||
),
|
||||
tags.map {
|
||||
SearchTag(
|
||||
null,
|
||||
mangaId,
|
||||
it.namespace,
|
||||
it.name,
|
||||
it.type
|
||||
)
|
||||
},
|
||||
titles.map {
|
||||
SearchTitle(
|
||||
null,
|
||||
mangaId,
|
||||
it.title,
|
||||
it.type
|
||||
)
|
||||
}
|
||||
tags.map {
|
||||
SearchTag(
|
||||
null,
|
||||
mangaId,
|
||||
it.namespace,
|
||||
it.name,
|
||||
it.type
|
||||
)
|
||||
},
|
||||
titles.map {
|
||||
SearchTitle(
|
||||
null,
|
||||
mangaId,
|
||||
it.title,
|
||||
it.type
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ abstract class RaisedSearchMetadata {
|
||||
* @return the property value.
|
||||
*/
|
||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
|
||||
thisRef.getTitleOfType(type)
|
||||
thisRef.getTitleOfType(type)
|
||||
|
||||
/**
|
||||
* Sets the value of the property for the given object.
|
||||
@@ -135,7 +135,7 @@ abstract class RaisedSearchMetadata {
|
||||
* @param value the value to set.
|
||||
*/
|
||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,22 +18,22 @@ import exh.metadata.sql.tables.SearchTagTable.COL_TYPE
|
||||
import exh.metadata.sql.tables.SearchTagTable.TABLE
|
||||
|
||||
class SearchTagTypeMapping : SQLiteTypeMapping<SearchTag>(
|
||||
SearchTagPutResolver(),
|
||||
SearchTagGetResolver(),
|
||||
SearchTagDeleteResolver()
|
||||
SearchTagPutResolver(),
|
||||
SearchTagGetResolver(),
|
||||
SearchTagDeleteResolver()
|
||||
)
|
||||
|
||||
class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: SearchTag) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: SearchTag) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: SearchTag) = ContentValues(5).apply {
|
||||
put(COL_ID, obj.id)
|
||||
@@ -47,19 +47,19 @@ class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
|
||||
class SearchTagGetResolver : DefaultGetResolver<SearchTag>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): SearchTag = SearchTag(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
)
|
||||
}
|
||||
|
||||
class SearchTagDeleteResolver : DefaultDeleteResolver<SearchTag>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: SearchTag) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -17,22 +17,22 @@ import exh.metadata.sql.tables.SearchTitleTable.COL_TYPE
|
||||
import exh.metadata.sql.tables.SearchTitleTable.TABLE
|
||||
|
||||
class SearchTitleTypeMapping : SQLiteTypeMapping<SearchTitle>(
|
||||
SearchTitlePutResolver(),
|
||||
SearchTitleGetResolver(),
|
||||
SearchTitleDeleteResolver()
|
||||
SearchTitlePutResolver(),
|
||||
SearchTitleGetResolver(),
|
||||
SearchTitleDeleteResolver()
|
||||
)
|
||||
|
||||
class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: SearchTitle) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: SearchTitle) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: SearchTitle) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
@@ -45,18 +45,18 @@ class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
|
||||
class SearchTitleGetResolver : DefaultGetResolver<SearchTitle>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): SearchTitle = SearchTitle(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
)
|
||||
}
|
||||
|
||||
class SearchTitleDeleteResolver : DefaultDeleteResolver<SearchTitle>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: SearchTitle) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
data class SearchTag(
|
||||
// Tag identifier, unique
|
||||
// Tag identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this tag is attached to
|
||||
// Metadata this tag is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Tag namespace
|
||||
// Tag namespace
|
||||
val namespace: String?,
|
||||
|
||||
// Tag name
|
||||
// Tag name
|
||||
val name: String,
|
||||
|
||||
// Tag type
|
||||
// Tag type
|
||||
val type: Int
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
data class SearchTitle(
|
||||
// Title identifier, unique
|
||||
// Title identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this title is attached to
|
||||
// Metadata this title is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Title
|
||||
// Title
|
||||
val title: String,
|
||||
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
val type: Int
|
||||
)
|
||||
|
||||
@@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTagTable
|
||||
|
||||
interface SearchTagQueries : DbProvider {
|
||||
fun getSearchTagsForManga(mangaId: Long) = db.get()
|
||||
.listOfObjects(SearchTag::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(SearchTag::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteSearchTagsForManga(mangaId: Long) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertSearchTag(searchTag: SearchTag) = db.put().`object`(searchTag).prepare()
|
||||
|
||||
@@ -31,10 +35,12 @@ interface SearchTagQueries : DbProvider {
|
||||
|
||||
fun deleteSearchTag(searchTag: SearchTag) = db.delete().`object`(searchTag).prepare()
|
||||
|
||||
fun deleteAllSearchTags() = db.delete().byQuery(DeleteQuery.builder()
|
||||
fun deleteAllSearchTags() = db.delete().byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setSearchTagsForManga(mangaId: Long, tags: List<SearchTag>) {
|
||||
db.inTransaction {
|
||||
|
||||
@@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTitleTable
|
||||
|
||||
interface SearchTitleQueries : DbProvider {
|
||||
fun getSearchTitlesForManga(mangaId: Long) = db.get()
|
||||
.listOfObjects(SearchTitle::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(SearchTitle::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteSearchTitlesForManga(mangaId: Long) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertSearchTitle(searchTitle: SearchTitle) = db.put().`object`(searchTitle).prepare()
|
||||
|
||||
@@ -31,10 +35,12 @@ interface SearchTitleQueries : DbProvider {
|
||||
|
||||
fun deleteSearchTitle(searchTitle: SearchTitle) = db.delete().`object`(searchTitle).prepare()
|
||||
|
||||
fun deleteAllSearchTitle() = db.delete().byQuery(DeleteQuery.builder()
|
||||
fun deleteAllSearchTitle() = db.delete().byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setSearchTitlesForManga(mangaId: Long, titles: List<SearchTitle>) {
|
||||
db.inTransaction {
|
||||
|
||||
@@ -17,7 +17,8 @@ object SearchMetadataTable {
|
||||
|
||||
// Insane foreign, primary key to avoid touch manga table
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_UPLOADER TEXT,
|
||||
$COL_EXTRA TEXT NOT NULL,
|
||||
|
||||
@@ -16,7 +16,8 @@ object SearchTagTable {
|
||||
const val COL_TYPE = "type"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_NAMESPACE TEXT,
|
||||
|
||||
@@ -14,7 +14,8 @@ object SearchTitleTable {
|
||||
const val COL_TYPE = "type"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
|
||||
@@ -9,13 +9,14 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
private val HIDE_SCRIPT = """
|
||||
document.querySelector("#forgot_button").style.visibility = "hidden";
|
||||
document.querySelector("#signup_button").style.visibility = "hidden";
|
||||
document.querySelector("#announcement").style.visibility = "hidden";
|
||||
document.querySelector("nav").style.visibility = "hidden";
|
||||
document.querySelector("footer").style.visibility = "hidden";
|
||||
""".trimIndent()
|
||||
private val HIDE_SCRIPT =
|
||||
"""
|
||||
document.querySelector("#forgot_button").style.visibility = "hidden";
|
||||
document.querySelector("#signup_button").style.visibility = "hidden";
|
||||
document.querySelector("#announcement").style.visibility = "hidden";
|
||||
document.querySelector("nav").style.visibility = "hidden";
|
||||
document.querySelector("footer").style.visibility = "hidden";
|
||||
""".trimIndent()
|
||||
|
||||
private fun verifyComplete(url: String): Boolean {
|
||||
return url.toHttpUrlOrNull()?.let { parsed ->
|
||||
@@ -28,14 +29,14 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
response.interceptAsHtml { doc ->
|
||||
if (doc.title().trim().equals("Login - MangaDex", true)) {
|
||||
BrowserActionActivity.launchAction(
|
||||
Injekt.get<Application>(),
|
||||
::verifyComplete,
|
||||
HIDE_SCRIPT,
|
||||
"https://mangadex.org/login",
|
||||
"Login",
|
||||
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
Injekt.get<Application>(),
|
||||
::verifyComplete,
|
||||
HIDE_SCRIPT,
|
||||
"https://mangadex.org/login",
|
||||
"Login",
|
||||
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -43,43 +44,43 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
}
|
||||
|
||||
val MANGADEX_SOURCE_IDS = listOf(
|
||||
2499283573021220255,
|
||||
8033579885162383068,
|
||||
1952071260038453057,
|
||||
2098905203823335614,
|
||||
5098537545549490547,
|
||||
4505830566611664829,
|
||||
9194073792736219759,
|
||||
6400665728063187402,
|
||||
4938773340256184018,
|
||||
5860541308324630662,
|
||||
5189216366882819742,
|
||||
2655149515337070132,
|
||||
1145824452519314725,
|
||||
3846770256925560569,
|
||||
3807502156582598786,
|
||||
4284949320785450865,
|
||||
5463447640980279236,
|
||||
8578871918181236609,
|
||||
6750440049024086587,
|
||||
3339599426223341161,
|
||||
5148895169070562838,
|
||||
1493666528525752601,
|
||||
1713554459881080228,
|
||||
4150470519566206911,
|
||||
1347402746269051958,
|
||||
3578612018159256808,
|
||||
425785191804166217,
|
||||
8254121249433835847,
|
||||
3260701926561129943,
|
||||
1411768577036936240,
|
||||
3285208643537017688,
|
||||
737986167355114438,
|
||||
1471784905273036181,
|
||||
5967745367608513818,
|
||||
3781216447842245147,
|
||||
4774459486579224459,
|
||||
4710920497926776490,
|
||||
5779037855201976894
|
||||
2499283573021220255,
|
||||
8033579885162383068,
|
||||
1952071260038453057,
|
||||
2098905203823335614,
|
||||
5098537545549490547,
|
||||
4505830566611664829,
|
||||
9194073792736219759,
|
||||
6400665728063187402,
|
||||
4938773340256184018,
|
||||
5860541308324630662,
|
||||
5189216366882819742,
|
||||
2655149515337070132,
|
||||
1145824452519314725,
|
||||
3846770256925560569,
|
||||
3807502156582598786,
|
||||
4284949320785450865,
|
||||
5463447640980279236,
|
||||
8578871918181236609,
|
||||
6750440049024086587,
|
||||
3339599426223341161,
|
||||
5148895169070562838,
|
||||
1493666528525752601,
|
||||
1713554459881080228,
|
||||
4150470519566206911,
|
||||
1347402746269051958,
|
||||
3578612018159256808,
|
||||
425785191804166217,
|
||||
8254121249433835847,
|
||||
3260701926561129943,
|
||||
1411768577036936240,
|
||||
3285208643537017688,
|
||||
737986167355114438,
|
||||
1471784905273036181,
|
||||
5967745367608513818,
|
||||
3781216447842245147,
|
||||
4774459486579224459,
|
||||
4710920497926776490,
|
||||
5779037855201976894
|
||||
)
|
||||
const val MANGADEX_DOMAIN = "mangadex.org"
|
||||
|
||||
@@ -16,8 +16,10 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
|
||||
}
|
||||
|
||||
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
||||
return ((EH_INTERCEPTORS[sourceId] ?: emptyList()) +
|
||||
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())).merge()
|
||||
return (
|
||||
(EH_INTERCEPTORS[sourceId] ?: emptyList()) +
|
||||
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())
|
||||
).merge()
|
||||
}
|
||||
|
||||
fun List<EHInterceptor>.merge(): EHInterceptor {
|
||||
@@ -30,12 +32,12 @@ fun List<EHInterceptor>.merge(): EHInterceptor {
|
||||
|
||||
private const val EH_UNIVERSAL_INTERCEPTOR = -1L
|
||||
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
|
||||
EH_UNIVERSAL_INTERCEPTOR to listOf(
|
||||
CAPTCHA_DETECTION_PATCH // Auto captcha detection
|
||||
),
|
||||
EH_UNIVERSAL_INTERCEPTOR to listOf(
|
||||
CAPTCHA_DETECTION_PATCH // Auto captcha detection
|
||||
),
|
||||
|
||||
// MangaDex login support
|
||||
*MANGADEX_SOURCE_IDS.map { id ->
|
||||
id to listOf(MANGADEX_LOGIN_PATCH)
|
||||
}.toTypedArray()
|
||||
// MangaDex login support
|
||||
*MANGADEX_SOURCE_IDS.map { id ->
|
||||
id to listOf(MANGADEX_LOGIN_PATCH)
|
||||
}.toTypedArray()
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
||||
// Found it, allow the user to solve this thing
|
||||
BrowserActionActivity.launchUniversal(
|
||||
Injekt.get<Application>(),
|
||||
sourceId,
|
||||
request.url.toString()
|
||||
Injekt.get<Application>(),
|
||||
sourceId,
|
||||
request.url.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ class SearchEngine {
|
||||
component: Text?
|
||||
): Pair<String, List<String>>? {
|
||||
val maybeLenientComponent = component?.let {
|
||||
if (!it.exact)
|
||||
it.asLenientTagQueries()
|
||||
else
|
||||
listOf(it.asQuery())
|
||||
if (!it.exact) {
|
||||
it.asLenientTagQueries()
|
||||
} else {
|
||||
listOf(it.asQuery())
|
||||
}
|
||||
}
|
||||
val componentTagQuery = maybeLenientComponent?.let {
|
||||
val params = mutableListOf<String>()
|
||||
@@ -25,11 +26,12 @@ class SearchEngine {
|
||||
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
||||
}
|
||||
return if (namespace != null) {
|
||||
var query = """
|
||||
var query =
|
||||
"""
|
||||
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
||||
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
val params = mutableListOf(escapeLike(namespace))
|
||||
if (componentTagQuery != null) {
|
||||
query += "\n AND ${componentTagQuery.first}"
|
||||
@@ -39,18 +41,20 @@ class SearchEngine {
|
||||
"$query)" to params
|
||||
} else if (component != null) {
|
||||
// Match title + tags
|
||||
val tagQuery = """
|
||||
val tagQuery =
|
||||
"""
|
||||
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||
WHERE ${componentTagQuery!!.first}
|
||||
""".trimIndent() to componentTagQuery.second
|
||||
""".trimIndent() to componentTagQuery.second
|
||||
|
||||
val titleQuery = """
|
||||
val titleQuery =
|
||||
"""
|
||||
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
|
||||
WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
|
||||
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
||||
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
||||
|
||||
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
|
||||
(tagQuery.second + titleQuery.second)
|
||||
(tagQuery.second + titleQuery.second)
|
||||
} else null
|
||||
}
|
||||
|
||||
@@ -86,22 +90,25 @@ class SearchEngine {
|
||||
}
|
||||
|
||||
val completeParams = mutableListOf<String>()
|
||||
var baseQuery = """
|
||||
var baseQuery =
|
||||
"""
|
||||
SELECT ${SearchMetadataTable.COL_MANGA_ID}
|
||||
FROM ${SearchMetadataTable.TABLE} meta
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
include.forEachIndexed { index, pair ->
|
||||
baseQuery += "\n" + ("""
|
||||
baseQuery += "\n" + (
|
||||
"""
|
||||
INNER JOIN ${pair.first} i$index
|
||||
ON i$index.$COL_MANGA_ID = meta.${SearchMetadataTable.COL_MANGA_ID}
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
completeParams += pair.second
|
||||
}
|
||||
|
||||
exclude.forEach {
|
||||
wheres += """
|
||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||
""".trimIndent()
|
||||
whereParams += it.second
|
||||
}
|
||||
@@ -196,8 +203,8 @@ class SearchEngine {
|
||||
|
||||
fun escapeLike(string: String): String {
|
||||
return string.replace("\\", "\\\\")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ class Text : QueryComponent() {
|
||||
fun asLenientTagQueries(): List<String> {
|
||||
if (lenientTagQueries == null) {
|
||||
lenientTagQueries = listOf(
|
||||
// Match beginning of tag
|
||||
rBaseBuilder().append("%").toString(),
|
||||
// Tag word matcher (that matches multiple words)
|
||||
// Can't make it match a single word in Realm :(
|
||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||
rBaseBuilder().append(" ").toString()
|
||||
// Match beginning of tag
|
||||
rBaseBuilder().append("%").toString(),
|
||||
// Tag word matcher (that matches multiple words)
|
||||
// Can't make it match a single word in Realm :(
|
||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||
rBaseBuilder().append(" ").toString()
|
||||
)
|
||||
}
|
||||
return lenientTagQueries!!
|
||||
@@ -52,11 +52,11 @@ class Text : QueryComponent() {
|
||||
return builder
|
||||
}
|
||||
|
||||
fun rawTextOnly() = if (rawText != null)
|
||||
fun rawTextOnly() = if (rawText != null) {
|
||||
rawText!!
|
||||
else {
|
||||
} else {
|
||||
rawText = components
|
||||
.joinToString(separator = "", transform = { it.rawText })
|
||||
.joinToString(separator = "", transform = { it.rawText })
|
||||
rawText!!
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@ class SmartSearchEngine(
|
||||
} else title
|
||||
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
||||
|
||||
if (searchResults.mangas.size == 1)
|
||||
if (searchResults.mangas.size == 1) {
|
||||
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
|
||||
}
|
||||
|
||||
searchResults.mangas.map {
|
||||
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)
|
||||
|
||||
@@ -7,38 +7,38 @@ object BlacklistedSources {
|
||||
val PERVEDEN_EN_EXT_SOURCES = listOf(4673633799850248749)
|
||||
val PERVEDEN_IT_EXT_SOURCES = listOf(1433898225963724122)
|
||||
val EHENTAI_EXT_SOURCES = listOf(
|
||||
8100626124886895451,
|
||||
57122881048805941,
|
||||
4678440076103929247,
|
||||
1876021963378735852,
|
||||
3955189842350477641,
|
||||
4348288691341764259,
|
||||
773611868725221145,
|
||||
5759417018342755550,
|
||||
825187715438990384,
|
||||
6116711405602166104,
|
||||
7151438547982231541,
|
||||
2171445159732592630,
|
||||
3032959619549451093,
|
||||
5980349886941016589,
|
||||
6073266008352078708,
|
||||
5499077866612745456,
|
||||
6140480779421365791
|
||||
8100626124886895451,
|
||||
57122881048805941,
|
||||
4678440076103929247,
|
||||
1876021963378735852,
|
||||
3955189842350477641,
|
||||
4348288691341764259,
|
||||
773611868725221145,
|
||||
5759417018342755550,
|
||||
825187715438990384,
|
||||
6116711405602166104,
|
||||
7151438547982231541,
|
||||
2171445159732592630,
|
||||
3032959619549451093,
|
||||
5980349886941016589,
|
||||
6073266008352078708,
|
||||
5499077866612745456,
|
||||
6140480779421365791
|
||||
)
|
||||
|
||||
val BLACKLISTED_EXT_SOURCES = NHENTAI_EXT_SOURCES +
|
||||
PERVEDEN_EN_EXT_SOURCES +
|
||||
PERVEDEN_IT_EXT_SOURCES +
|
||||
EHENTAI_EXT_SOURCES
|
||||
PERVEDEN_EN_EXT_SOURCES +
|
||||
PERVEDEN_IT_EXT_SOURCES +
|
||||
EHENTAI_EXT_SOURCES
|
||||
|
||||
val BLACKLISTED_EXTENSIONS = listOf(
|
||||
"eu.kanade.tachiyomi.extension.all.ehentai",
|
||||
"eu.kanade.tachiyomi.extension.all.nhentai",
|
||||
"eu.kanade.tachiyomi.extension.en.perveden",
|
||||
"eu.kanade.tachiyomi.extension.it.perveden"
|
||||
"eu.kanade.tachiyomi.extension.all.ehentai",
|
||||
"eu.kanade.tachiyomi.extension.all.nhentai",
|
||||
"eu.kanade.tachiyomi.extension.en.perveden",
|
||||
"eu.kanade.tachiyomi.extension.it.perveden"
|
||||
)
|
||||
|
||||
val HIDDEN_SOURCES = listOf(
|
||||
MERGED_SOURCE_ID
|
||||
MERGED_SOURCE_ID
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -26,7 +26,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@@ -36,7 +36,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -44,7 +44,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@@ -52,7 +52,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -60,7 +60,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@@ -68,7 +68,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@@ -76,7 +76,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@@ -84,7 +84,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@@ -92,7 +92,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
@@ -240,7 +240,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
|
||||
private fun ensureDelegateCompatible() {
|
||||
if (versionId != delegate.versionId ||
|
||||
lang != delegate.lang) {
|
||||
lang != delegate.lang
|
||||
) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class EnhancedHttpSource(
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -31,7 +31,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@@ -41,7 +41,7 @@ class EnhancedHttpSource(
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -49,7 +49,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@@ -57,7 +57,7 @@ class EnhancedHttpSource(
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@@ -65,7 +65,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@@ -73,7 +73,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@@ -81,7 +81,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@@ -89,7 +89,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@@ -97,7 +97,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
@@ -153,7 +153,7 @@ class EnhancedHttpSource(
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
@@ -209,7 +209,7 @@ class EnhancedHttpSource(
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
|
||||
@@ -14,7 +14,7 @@ class ConfiguringDialogController : DialogController() {
|
||||
private var materialDialog: MaterialDialog? = null
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
if (savedViewState == null)
|
||||
if (savedViewState == null) {
|
||||
thread {
|
||||
try {
|
||||
EHConfigurator().configureAll()
|
||||
@@ -25,10 +25,10 @@ class ConfiguringDialogController : DialogController() {
|
||||
activity?.let {
|
||||
it.runOnUiThread {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Configuration failed!")
|
||||
.message(text = "An error occurred during the configuration process: " + e.message)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "Configuration failed!")
|
||||
.message(text = "An error occurred during the configuration process: " + e.message)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
Timber.e(e, "Configuration error!")
|
||||
@@ -37,14 +37,15 @@ class ConfiguringDialogController : DialogController() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = "Uploading settings to server")
|
||||
.message(text = "Please wait, this may take some time...")
|
||||
.cancelable(false)
|
||||
.also {
|
||||
materialDialog = it
|
||||
}
|
||||
.title(text = "Uploading settings to server")
|
||||
.message(text = "Please wait, this may take some time...")
|
||||
.cancelable(false)
|
||||
.also {
|
||||
materialDialog = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
|
||||
@@ -18,11 +18,11 @@ class EHConfigurator {
|
||||
private val sources: SourceManager by injectLazy()
|
||||
|
||||
private val configuratorClient = OkHttpClient.Builder()
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
|
||||
.addHeader("Cookie", cookiesHeader(sp))
|
||||
.addHeader("Cookie", cookiesHeader(sp))
|
||||
|
||||
private fun EHentai.execProfileActions(
|
||||
action: String,
|
||||
@@ -30,15 +30,19 @@ class EHConfigurator {
|
||||
set: String,
|
||||
sp: Int
|
||||
) =
|
||||
configuratorClient.newCall(requestWithCreds(sp)
|
||||
configuratorClient.newCall(
|
||||
requestWithCreds(sp)
|
||||
.url(uconfigUrl)
|
||||
.post(FormBody.Builder()
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("profile_action", action)
|
||||
.add("profile_name", name)
|
||||
.add("profile_set", set)
|
||||
.build())
|
||||
.build())
|
||||
.execute()
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.execute()
|
||||
|
||||
private val EHentai.uconfigUrl get() = baseUrl + UCONFIG_URL
|
||||
|
||||
@@ -47,10 +51,12 @@ class EHConfigurator {
|
||||
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
|
||||
|
||||
// Get hath perks
|
||||
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds()
|
||||
val perksPage = configuratorClient.newCall(
|
||||
ehSource.requestWithCreds()
|
||||
.url(HATH_PERKS_URL)
|
||||
.build())
|
||||
.execute().asJsoup()
|
||||
.build()
|
||||
)
|
||||
.execute().asJsoup()
|
||||
|
||||
val hathPerks = EHHathPerksResponse()
|
||||
|
||||
@@ -97,24 +103,29 @@ class EHConfigurator {
|
||||
}
|
||||
|
||||
// No profile slots left :(
|
||||
if (availableProfiles.isEmpty())
|
||||
if (availableProfiles.isEmpty()) {
|
||||
throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
|
||||
|
||||
}
|
||||
// Create profile in available slot
|
||||
|
||||
val slot = availableProfiles.first()
|
||||
val response = source.execProfileActions("create",
|
||||
PROFILE_NAME,
|
||||
slot.toString(),
|
||||
1)
|
||||
val response = source.execProfileActions(
|
||||
"create",
|
||||
PROFILE_NAME,
|
||||
slot.toString(),
|
||||
1
|
||||
)
|
||||
|
||||
// Build new profile
|
||||
val form = EhUConfigBuilder().build(hathPerks)
|
||||
|
||||
// Send new profile to server
|
||||
configuratorClient.newCall(source.requestWithCreds(sp = slot)
|
||||
configuratorClient.newCall(
|
||||
source.requestWithCreds(sp = slot)
|
||||
.url(source.uconfigUrl)
|
||||
.post(form)
|
||||
.build()).execute()
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
// Persist slot + sk
|
||||
source.spPref().set(slot)
|
||||
@@ -129,12 +140,15 @@ class EHConfigurator {
|
||||
it.startsWith("hath_perks=")
|
||||
}?.removePrefix("hath_perks=")?.substringBefore(';')
|
||||
|
||||
if (keyCookie != null)
|
||||
if (keyCookie != null) {
|
||||
prefs.eh_settingsKey().set(keyCookie)
|
||||
if (sessionCookie != null)
|
||||
}
|
||||
if (sessionCookie != null) {
|
||||
prefs.eh_sessionCookie().set(sessionCookie)
|
||||
if (hathPerksCookie != null)
|
||||
}
|
||||
if (hathPerksCookie != null) {
|
||||
prefs.eh_hathPerksCookies().set(hathPerksCookie)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -11,9 +11,11 @@ class EhUConfigBuilder {
|
||||
fun build(hathPerks: EHHathPerksResponse): FormBody {
|
||||
val configItems = mutableListOf<ConfigItem>()
|
||||
|
||||
configItems += when (prefs.imageQuality()
|
||||
configItems += when (
|
||||
prefs.imageQuality()
|
||||
.getOrDefault()
|
||||
.toLowerCase()) {
|
||||
.toLowerCase()
|
||||
) {
|
||||
"ovrs_2400" -> Entry.ImageSize.`2400`
|
||||
"ovrs_1600" -> Entry.ImageSize.`1600`
|
||||
"high" -> Entry.ImageSize.`1280`
|
||||
@@ -23,20 +25,23 @@ class EhUConfigBuilder {
|
||||
else -> Entry.ImageSize.AUTO
|
||||
}
|
||||
|
||||
configItems += if (prefs.useHentaiAtHome().getOrDefault())
|
||||
configItems += if (prefs.useHentaiAtHome().getOrDefault()) {
|
||||
Entry.UseHentaiAtHome.YES
|
||||
else
|
||||
} else {
|
||||
Entry.UseHentaiAtHome.NO
|
||||
}
|
||||
|
||||
configItems += if (prefs.useJapaneseTitle().getOrDefault())
|
||||
configItems += if (prefs.useJapaneseTitle().getOrDefault()) {
|
||||
Entry.TitleDisplayLanguage.JAPANESE
|
||||
else
|
||||
} else {
|
||||
Entry.TitleDisplayLanguage.DEFAULT
|
||||
}
|
||||
|
||||
configItems += if (prefs.eh_useOriginalImages().getOrDefault())
|
||||
configItems += if (prefs.eh_useOriginalImages().getOrDefault()) {
|
||||
Entry.UseOriginalImages.YES
|
||||
else
|
||||
} else {
|
||||
Entry.UseOriginalImages.NO
|
||||
}
|
||||
|
||||
configItems += when {
|
||||
hathPerks.allThumbs -> Entry.ThumbnailRows.`40`
|
||||
|
||||
@@ -14,25 +14,29 @@ class WarnConfigureDialogController : DialogController() {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = "Settings profile note")
|
||||
.message(text = """
|
||||
.title(text = "Settings profile note")
|
||||
.message(
|
||||
text =
|
||||
"""
|
||||
The app will now add a new settings profile on E-Hentai and ExHentai to optimize app performance. Please ensure that you have less than three profiles on both sites.
|
||||
|
||||
If you have no idea what settings profiles are, then it probably doesn't matter, just hit 'OK'.
|
||||
""".trimIndent())
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSettingsUploadWarning().set(false)
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
.cancelable(false)
|
||||
""".trimIndent()
|
||||
)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSettingsUploadWarning().set(false)
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
.cancelable(false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun uploadSettings(router: Router) {
|
||||
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get())
|
||||
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get()) {
|
||||
WarnConfigureDialogController().showDialog(router)
|
||||
else
|
||||
} else {
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,66 +49,68 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
|
||||
val progressSubscriptions = CompositeSubscription()
|
||||
|
||||
presenter.currentlyAddingRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
progressSubscriptions.clear()
|
||||
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
showProgress(this)
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.combineLatest(presenter.progressTotalRelay) { progress, total ->
|
||||
// Show hide dismiss button
|
||||
binding.progressDismissBtn.visibility =
|
||||
if (progress == total)
|
||||
View.VISIBLE
|
||||
else View.GONE
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
progressSubscriptions.clear()
|
||||
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
showProgress(this)
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.combineLatest(presenter.progressTotalRelay) { progress, total ->
|
||||
// Show hide dismiss button
|
||||
binding.progressDismissBtn.visibility =
|
||||
if (progress == total) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
formatProgress(progress, total)
|
||||
}.subscribeUntilDestroy {
|
||||
formatProgress(progress, total)
|
||||
}.subscribeUntilDestroy {
|
||||
binding.progressText.text = it
|
||||
}
|
||||
|
||||
progressSubscriptions += presenter.progressTotalRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.max = it
|
||||
}
|
||||
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.progress = it
|
||||
}
|
||||
|
||||
presenter.eventRelay
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribeUntilDestroy {
|
||||
binding.progressLog.append("$it\n")
|
||||
}?.let {
|
||||
progressSubscriptions += it
|
||||
progressSubscriptions += presenter.progressTotalRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.max = it
|
||||
}
|
||||
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
hideProgress(this)
|
||||
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
||||
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.progress = it
|
||||
}
|
||||
|
||||
presenter.eventRelay
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribeUntilDestroy {
|
||||
binding.progressLog.append("$it\n")
|
||||
}?.let {
|
||||
progressSubscriptions += it
|
||||
}
|
||||
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
hideProgress(this)
|
||||
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val View.progressViews
|
||||
get() = listOf(
|
||||
binding.progressTitleView,
|
||||
binding.progressLogWrapper,
|
||||
binding.progressBar,
|
||||
binding.progressText,
|
||||
binding.progressDismissBtn
|
||||
binding.progressTitleView,
|
||||
binding.progressLogWrapper,
|
||||
binding.progressBar,
|
||||
binding.progressText,
|
||||
binding.progressDismissBtn
|
||||
)
|
||||
|
||||
private val View.inputViews
|
||||
get() = listOf(
|
||||
binding.inputTitleView,
|
||||
binding.galleriesBox,
|
||||
binding.btnAddGalleries
|
||||
binding.inputTitleView,
|
||||
binding.galleriesBox,
|
||||
binding.btnAddGalleries
|
||||
)
|
||||
|
||||
private var List<View>.visibility: Int
|
||||
@@ -144,12 +146,12 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
|
||||
private fun noGalleriesSpecified() {
|
||||
activity?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "No galleries to add!")
|
||||
.message(text = "You must specify at least one gallery to add!")
|
||||
.positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
.title(text = "No galleries to add!")
|
||||
.message(text = "You must specify at least one gallery to add!")
|
||||
.positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,14 @@ class BatchAddPresenter : BasePresenter<BatchAddController>() {
|
||||
failed.add(s)
|
||||
}
|
||||
progressRelay.call(i + 1)
|
||||
eventRelay?.call((when (result) {
|
||||
is GalleryAddEvent.Success -> "[OK]"
|
||||
is GalleryAddEvent.Fail -> "[ERROR]"
|
||||
}) + " " + result.logMessage)
|
||||
eventRelay?.call(
|
||||
(
|
||||
when (result) {
|
||||
is GalleryAddEvent.Success -> "[OK]"
|
||||
is GalleryAddEvent.Fail -> "[ERROR]"
|
||||
}
|
||||
) + " " + result.logMessage
|
||||
)
|
||||
}
|
||||
|
||||
// Show report
|
||||
|
||||
@@ -26,9 +26,9 @@ class AutoSolvingWebViewClient(
|
||||
val doc = response.asJsoup()
|
||||
doc.body().appendChild(Element("script").appendChild(DataNode(CROSS_WINDOW_SCRIPT_INNER)))
|
||||
return WebResourceResponse(
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
|
||||
)
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
|
||||
@@ -14,8 +14,9 @@ open class BasicWebViewClient(
|
||||
if (verifyComplete(url)) {
|
||||
activity.finish()
|
||||
} else {
|
||||
if (injectScript != null)
|
||||
if (injectScript != null) {
|
||||
view.evaluateJavascript("(function() {$injectScript})();", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,24 +62,27 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
|
||||
val source = if (originalSource != null) {
|
||||
originalSource as? ActionCompletionVerifier
|
||||
?: run {
|
||||
(originalSource as? HttpSource)?.let {
|
||||
NoopActionCompletionVerifier(it)
|
||||
}
|
||||
?: run {
|
||||
(originalSource as? HttpSource)?.let {
|
||||
NoopActionCompletionVerifier(it)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
val headers = ((source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||
val headers = (
|
||||
(source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||
|
||||
val cookies: HashMap<String, String>? =
|
||||
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
||||
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
||||
|
||||
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE") val verifyComplete = if (source != null) {
|
||||
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE")
|
||||
val verifyComplete = if (source != null) {
|
||||
source::verifyComplete!!
|
||||
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
||||
|
||||
@@ -139,10 +142,12 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
webview.webViewClient = if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
||||
// Fetch auto-solve credentials early for speed
|
||||
credentialsObservable = httpClient.newCall(Request.Builder()
|
||||
// Rob demo credentials
|
||||
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
||||
.build())
|
||||
credentialsObservable = httpClient.newCall(
|
||||
Request.Builder()
|
||||
// Rob demo credentials
|
||||
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
||||
.build()
|
||||
)
|
||||
.asObservableSuccess()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map {
|
||||
@@ -176,12 +181,12 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
runOnUiThread {
|
||||
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
||||
MaterialDialog(this)
|
||||
.title(text = "Captcha solve failure")
|
||||
.message(text = "Failed to auto-solve the captcha!")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "Captcha solve failure")
|
||||
.message(text = "Failed to auto-solve the captcha!")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +197,19 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
when (stage) {
|
||||
STAGE_CHECKBOX -> {
|
||||
if (result!!.toBoolean()) {
|
||||
webview.postDelayed({
|
||||
getAudioButtonLocation(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
getAudioButtonLocation(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
doStageCheckbox(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
doStageCheckbox(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
||||
@@ -216,31 +227,43 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
doStageDownloadAudio(loopId)
|
||||
}
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
getAudioButtonLocation(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
getAudioButtonLocation(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_DOWNLOAD_AUDIO -> {
|
||||
if (result != null) {
|
||||
Timber.d("Got audio URL: $result")
|
||||
performRecognize(result)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
Timber.d("Got audio transcript: $it")
|
||||
webview.post {
|
||||
typeResult(loopId, it!!
|
||||
typeResult(
|
||||
loopId,
|
||||
it!!
|
||||
.replace(TRANSCRIPT_CLEANER_REGEX, "")
|
||||
.replace(SPACE_DEDUPE_REGEX, " ")
|
||||
.trim())
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
captchaSolveFail()
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
doStageDownloadAudio(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
doStageDownloadAudio(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_TYPE_RESULT -> {
|
||||
@@ -256,27 +279,37 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
fun performRecognize(url: String): Single<String> {
|
||||
return credentialsObservable.flatMap { token ->
|
||||
httpClient.newCall(Request.Builder()
|
||||
httpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.build()).asObservableSuccess().map {
|
||||
.build()
|
||||
).asObservableSuccess().map {
|
||||
token to it
|
||||
}
|
||||
}.flatMap { (token, response) ->
|
||||
val audioFile = response.body!!.bytes()
|
||||
|
||||
httpClient.newCall(Request.Builder()
|
||||
.url("https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!!
|
||||
httpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(
|
||||
"https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("watson-token", token)
|
||||
.build())
|
||||
.post(MultipartBody.Builder()
|
||||
.build()
|
||||
)
|
||||
.post(
|
||||
MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("jsonDescription", RECOGNIZE_JSON)
|
||||
.addFormDataPart("audio.mp3",
|
||||
"audio.mp3",
|
||||
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile))
|
||||
.build())
|
||||
.build()).asObservableSuccess()
|
||||
.addFormDataPart(
|
||||
"audio.mp3",
|
||||
"audio.mp3",
|
||||
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
).asObservableSuccess()
|
||||
}.map { response ->
|
||||
JsonParser.parseString(response.body!!.string())["results"][0]["alternatives"][0]["transcript"].string.trim()
|
||||
}.toSingle()
|
||||
@@ -285,7 +318,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
fun doStageCheckbox(loopId: String) {
|
||||
if (loopId != currentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@@ -307,11 +341,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback("false", '$loopId', $STAGE_CHECKBOX);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun getAudioButtonLocation(loopId: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@@ -339,11 +376,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun doStageDownloadAudio(loopId: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@@ -364,11 +404,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback(null, '$loopId', $STAGE_DOWNLOAD_AUDIO);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun typeResult(loopId: String, result: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@@ -392,7 +435,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback("false", '$loopId', $STAGE_TYPE_RESULT);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun beginSolveLoop() {
|
||||
@@ -419,12 +464,16 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
} else {
|
||||
val savedStrictValidationStartTime = strictValidationStartTime
|
||||
if (savedStrictValidationStartTime != null &&
|
||||
System.currentTimeMillis() > savedStrictValidationStartTime) {
|
||||
System.currentTimeMillis() > savedStrictValidationStartTime
|
||||
) {
|
||||
captchaSolveFail()
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
runValidateCaptcha(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
runValidateCaptcha(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,7 +481,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
fun runValidateCaptcha(loopId: String) {
|
||||
if (loopId != validateCurrentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@@ -453,7 +503,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.validateCaptchaCallback(false, '$loopId');
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun beginValidateCaptchaLoop() {
|
||||
@@ -502,7 +554,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
const val STAGE_DOWNLOAD_AUDIO = 2
|
||||
const val STAGE_TYPE_RESULT = 3
|
||||
|
||||
val CROSS_WINDOW_SCRIPT_OUTER = """
|
||||
val CROSS_WINDOW_SCRIPT_OUTER =
|
||||
"""
|
||||
function cwmExec(element, code, cb) {
|
||||
console.log(">>> [CWM-Outer] Running: " + code);
|
||||
let runId = Math.random();
|
||||
@@ -523,9 +576,10 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
let runRequest = { id: runId, code: code };
|
||||
element.contentWindow.postMessage("exh-" + JSON.stringify(runRequest), "*");
|
||||
}
|
||||
""".trimIndent().replace("\n", "")
|
||||
""".trimIndent().replace("\n", "")
|
||||
|
||||
val CROSS_WINDOW_SCRIPT_INNER = """
|
||||
val CROSS_WINDOW_SCRIPT_INNER =
|
||||
"""
|
||||
window.addEventListener('message', function(event) {
|
||||
if(typeof event.data === "string" && event.data.startsWith("exh-")) {
|
||||
let request = JSON.parse(event.data.substring(4));
|
||||
@@ -538,9 +592,10 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}, false);
|
||||
console.log(">>> [CWM-Inner] Loaded!");
|
||||
alert("exh-");
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val SOLVE_UI_SCRIPT_SHOW = """
|
||||
val SOLVE_UI_SCRIPT_SHOW =
|
||||
"""
|
||||
(function() {
|
||||
let exh_overlay = document.createElement("div");
|
||||
exh_overlay.id = "exh_overlay";
|
||||
@@ -568,18 +623,20 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh_otext.textContent = "Solving captcha..."
|
||||
document.body.appendChild(exh_otext);
|
||||
})();
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val SOLVE_UI_SCRIPT_HIDE = """
|
||||
val SOLVE_UI_SCRIPT_HIDE =
|
||||
"""
|
||||
(function() {
|
||||
let exh_overlay = document.getElementById("exh_overlay");
|
||||
let exh_otext = document.getElementById("exh_otext");
|
||||
if(exh_overlay != null) exh_overlay.remove();
|
||||
if(exh_otext != null) exh_otext.remove();
|
||||
})();
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val RECOGNIZE_JSON = """
|
||||
val RECOGNIZE_JSON =
|
||||
"""
|
||||
{
|
||||
"part_content_type": "audio/mp3",
|
||||
"keywords": [],
|
||||
@@ -596,15 +653,15 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
"customGrammarWords": [],
|
||||
"action": "recognize"
|
||||
}
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
|
||||
val SPACE_DEDUPE_REGEX = Regex(" +")
|
||||
|
||||
private fun baseIntent(context: Context) =
|
||||
Intent(context, BrowserActionActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
Intent(context, BrowserActionActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
fun launchCaptcha(
|
||||
context: Context,
|
||||
@@ -689,8 +746,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source),
|
||||
ActionCompletionVerifier {
|
||||
class NoopActionCompletionVerifier(private val source: HttpSource) :
|
||||
DelegatedHttpSource(source),
|
||||
ActionCompletionVerifier {
|
||||
override val versionId get() = source.versionId
|
||||
override val lang: String get() = source.lang
|
||||
|
||||
|
||||
@@ -37,43 +37,43 @@ open class HeadersInjectingWebViewClient(
|
||||
|
||||
companion object {
|
||||
private val FALLBACK_REASON_PHRASES = mapOf(
|
||||
100 to "Continue",
|
||||
101 to "Switching Protocols",
|
||||
200 to "OK",
|
||||
201 to "Created",
|
||||
202 to "Accepted",
|
||||
203 to "Non-Authoritative Information",
|
||||
204 to "No Content",
|
||||
205 to "Reset Content",
|
||||
206 to "Partial Content",
|
||||
300 to "Multiple Choices",
|
||||
301 to "Moved Permanently",
|
||||
302 to "Moved Temporarily",
|
||||
303 to "See Other",
|
||||
304 to "Not Modified",
|
||||
305 to "Use Proxy",
|
||||
400 to "Bad Request",
|
||||
401 to "Unauthorized",
|
||||
402 to "Payment Required",
|
||||
403 to "Forbidden",
|
||||
404 to "Not Found",
|
||||
405 to "Method Not Allowed",
|
||||
406 to "Not Acceptable",
|
||||
407 to "Proxy Authentication Required",
|
||||
408 to "Request Time-out",
|
||||
409 to "Conflict",
|
||||
410 to "Gone",
|
||||
411 to "Length Required",
|
||||
412 to "Precondition Failed",
|
||||
413 to "Request Entity Too Large",
|
||||
414 to "Request-URI Too Large",
|
||||
415 to "Unsupported Media Type",
|
||||
500 to "Internal Server Error",
|
||||
501 to "Not Implemented",
|
||||
502 to "Bad Gateway",
|
||||
503 to "Service Unavailable",
|
||||
504 to "Gateway Time-out",
|
||||
505 to "HTTP Version not supported"
|
||||
100 to "Continue",
|
||||
101 to "Switching Protocols",
|
||||
200 to "OK",
|
||||
201 to "Created",
|
||||
202 to "Accepted",
|
||||
203 to "Non-Authoritative Information",
|
||||
204 to "No Content",
|
||||
205 to "Reset Content",
|
||||
206 to "Partial Content",
|
||||
300 to "Multiple Choices",
|
||||
301 to "Moved Permanently",
|
||||
302 to "Moved Temporarily",
|
||||
303 to "See Other",
|
||||
304 to "Not Modified",
|
||||
305 to "Use Proxy",
|
||||
400 to "Bad Request",
|
||||
401 to "Unauthorized",
|
||||
402 to "Payment Required",
|
||||
403 to "Forbidden",
|
||||
404 to "Not Found",
|
||||
405 to "Method Not Allowed",
|
||||
406 to "Not Acceptable",
|
||||
407 to "Proxy Authentication Required",
|
||||
408 to "Request Time-out",
|
||||
409 to "Conflict",
|
||||
410 to "Gone",
|
||||
411 to "Length Required",
|
||||
412 to "Precondition Failed",
|
||||
413 to "Request Entity Too Large",
|
||||
414 to "Request-URI Too Large",
|
||||
415 to "Unsupported Media Type",
|
||||
500 to "Internal Server Error",
|
||||
501 to "Not Implemented",
|
||||
502 to "Bad Gateway",
|
||||
503 to "Service Unavailable",
|
||||
504 to "Gateway Time-out",
|
||||
505 to "HTTP Version not supported"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import okhttp3.Request
|
||||
|
||||
fun WebResourceRequest.toOkHttpRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.method(method, null)
|
||||
.url(url.toString())
|
||||
.method(method, null)
|
||||
|
||||
requestHeaders.entries.forEach { (t, u) ->
|
||||
request.addHeader(t, u)
|
||||
|
||||
@@ -54,33 +54,35 @@ class InterceptActivity : BaseRxActivity<EhActivityInterceptBinding, InterceptAc
|
||||
super.onStart()
|
||||
statusSubscription?.unsubscribe()
|
||||
statusSubscription = presenter.status
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is InterceptResult.Success -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Launching app..."
|
||||
onBackPressed()
|
||||
startActivity(Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, it.mangaId))
|
||||
}
|
||||
is InterceptResult.Failure -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Error: ${it.reason}"
|
||||
MaterialDialog(this)
|
||||
.title(text = "Error")
|
||||
.message(text = "Could not open this gallery:\n\n${it.reason}")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.onCancel { onBackPressed() }
|
||||
.onDismiss { onBackPressed() }
|
||||
.show()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is InterceptResult.Success -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Launching app..."
|
||||
onBackPressed()
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, it.mangaId)
|
||||
)
|
||||
}
|
||||
is InterceptResult.Failure -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Error: ${it.reason}"
|
||||
MaterialDialog(this)
|
||||
.title(text = "Error")
|
||||
.message(text = "Could not open this gallery:\n\n${it.reason}")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.onCancel { onBackPressed() }
|
||||
.onDismiss { onBackPressed() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
||||
@@ -21,12 +21,14 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||
thread {
|
||||
val result = galleryAdder.addGallery(gallery)
|
||||
|
||||
status.onNext(when (result) {
|
||||
is GalleryAddEvent.Success -> result.manga.id?.let {
|
||||
InterceptResult.Success(it)
|
||||
} ?: InterceptResult.Failure("Manga ID is null!")
|
||||
is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage)
|
||||
})
|
||||
status.onNext(
|
||||
when (result) {
|
||||
is GalleryAddEvent.Success -> result.manga.id?.let {
|
||||
InterceptResult.Success(it)
|
||||
} ?: InterceptResult.Failure("Manga ID is null!")
|
||||
is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,18 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
val fingerprintSupported
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered()
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered()
|
||||
|
||||
val useFingerprint
|
||||
get() = fingerprintSupported &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onAttached() {
|
||||
@@ -44,29 +44,32 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
if (fingerprintSupported) {
|
||||
updateSummary()
|
||||
onChange {
|
||||
if (it as Boolean)
|
||||
if (it as Boolean) {
|
||||
tryChange()
|
||||
else
|
||||
} else {
|
||||
prefs.eh_lockUseFingerprint().set(false)
|
||||
}
|
||||
!it
|
||||
}
|
||||
} else {
|
||||
title = "Fingerprint unsupported"
|
||||
shouldDisableView = true
|
||||
summary = if (!Reprint.hasFingerprintRegistered())
|
||||
summary = if (!Reprint.hasFingerprintRegistered()) {
|
||||
"No fingerprints enrolled!"
|
||||
else
|
||||
} else {
|
||||
"Fingerprint unlock is unsupported on this device!"
|
||||
}
|
||||
onChange { false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSummary() {
|
||||
isChecked = useFingerprint
|
||||
title = if (isChecked)
|
||||
title = if (isChecked) {
|
||||
"Fingerprint enabled"
|
||||
else
|
||||
} else {
|
||||
"Fingerprint disabled"
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@@ -74,9 +77,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
val statusTextView = TextView(context).apply {
|
||||
text = "Please touch the fingerprint sensor"
|
||||
val size = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
setPadding(0, 0, dpToPx(context, 8), 0)
|
||||
@@ -84,9 +89,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
}
|
||||
val iconView = SwirlView(context).apply {
|
||||
val size = dpToPx(context, 30)
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
}
|
||||
@@ -96,9 +103,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
orientation = LinearLayoutCompat.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT
|
||||
layoutParams = (layoutParams ?: LinearLayoutCompat.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: LinearLayoutCompat.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
val pSize = dpToPx(context, 24)
|
||||
@@ -109,39 +118,39 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
addView(iconView)
|
||||
}
|
||||
val dialog = MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification")
|
||||
.customView(view = linearLayout)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.title(text = "Fingerprint verification")
|
||||
.customView(view = linearLayout)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
dialog.show()
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
val subscription = RxReprint.authenticate()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
when (result.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> {
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
prefs.eh_lockUseFingerprint().set(true)
|
||||
dialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> {
|
||||
iconView.setState(SwirlView.State.ERROR)
|
||||
statusTextView.text = result.errorMessage
|
||||
}
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification failed!")
|
||||
.message(text = result.errorMessage)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
when (result.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> {
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
prefs.eh_lockUseFingerprint().set(true)
|
||||
dialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> {
|
||||
iconView.setState(SwirlView.State.ERROR)
|
||||
statusTextView.text = result.errorMessage
|
||||
}
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification failed!")
|
||||
.message(text = result.errorMessage)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
|
||||
@@ -20,19 +20,21 @@ object LockActivityDelegate {
|
||||
private val uiScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
fun doLock(router: Router, animate: Boolean = false) {
|
||||
router.pushController(RouterTransaction.with(LockController())
|
||||
.popChangeHandler(LockChangeHandler(animate)))
|
||||
router.pushController(
|
||||
RouterTransaction.with(LockController())
|
||||
.popChangeHandler(LockChangeHandler(animate))
|
||||
)
|
||||
}
|
||||
|
||||
fun onCreate(activity: FragmentActivity) {
|
||||
preferences.secureScreen().asFlow()
|
||||
.onEach {
|
||||
if (it) {
|
||||
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
.onEach {
|
||||
if (it) {
|
||||
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
.launchIn(uiScope)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@ class LockChangeHandler : AnimatorChangeHandler {
|
||||
override fun resetFromView(from: View) {}
|
||||
|
||||
override fun copy(): ControllerChangeHandler =
|
||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
closeLock()
|
||||
} else {
|
||||
MaterialDialog(context)
|
||||
.title(text = "PIN code incorrect")
|
||||
.message(text = "The PIN code you entered is incorrect. Please try again.")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "PIN code incorrect")
|
||||
.message(text = "The PIN code you entered is incorrect. Please try again.")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
binding.pinLockView.resetPinLockView()
|
||||
}
|
||||
}
|
||||
@@ -79,9 +79,11 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
binding.swirlContainer.removeAllViews()
|
||||
val icon = SwirlView(context).apply {
|
||||
val size = dpToPx(context, 60)
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
|
||||
@@ -92,29 +94,30 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
setBackgroundColor(lockColor)
|
||||
val bgColor = resolvColor(android.R.attr.colorBackground)
|
||||
// Disable elevation if lock color is same as background color
|
||||
if (lockColor == bgColor)
|
||||
if (lockColor == bgColor) {
|
||||
this@with.swirl_container.cardElevation = 0f
|
||||
}
|
||||
setState(SwirlView.State.OFF, true)
|
||||
}
|
||||
binding.swirlContainer.addView(icon)
|
||||
icon.setState(SwirlView.State.ON)
|
||||
RxReprint.authenticate()
|
||||
.subscribeUntilDetach {
|
||||
when (it.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> closeLock()
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint error!")
|
||||
.message(text = it.errorMessage)
|
||||
.cancelable(false)
|
||||
.cancelOnTouchOutside(false)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
icon.setState(SwirlView.State.OFF)
|
||||
}
|
||||
.subscribeUntilDetach {
|
||||
when (it.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> closeLock()
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint error!")
|
||||
.message(text = it.errorMessage)
|
||||
.cancelable(false)
|
||||
.cancelOnTouchOutside(false)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
icon.setState(SwirlView.State.OFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.swirlContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
private val secureRandom by lazy { SecureRandom() }
|
||||
|
||||
@@ -46,28 +46,28 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
fun tryChange() {
|
||||
if (!notifyLockSecurity(context)) {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Lock application")
|
||||
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
// .inputRangeRes(0, 10, R.color.material_red_500)
|
||||
// .inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
|
||||
val progressDialog = MaterialDialog(context)
|
||||
.title(text = "Saving password")
|
||||
.cancelable(false)
|
||||
progressDialog.show()
|
||||
Observable.fromCallable {
|
||||
savePassword(c.toString())
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
}
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
.title(text = "Lock application")
|
||||
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
// .inputRangeRes(0, 10, R.color.material_red_500)
|
||||
// .inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
|
||||
val progressDialog = MaterialDialog(context)
|
||||
.title(text = "Saving password")
|
||||
.cancelable(false)
|
||||
progressDialog.show()
|
||||
Observable.fromCallable {
|
||||
savePassword(c.toString())
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
}
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class LockPresenter : BasePresenter<LockController>() {
|
||||
|
||||
val useFingerprint
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered() &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered() &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ fun sha512(passwordToHash: String, salt: String): String {
|
||||
*/
|
||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) =
|
||||
prefs.eh_lockHash().get() != null &&
|
||||
prefs.eh_lockSalt().get() != null &&
|
||||
prefs.eh_lockLength().getOrDefault() != -1
|
||||
prefs.eh_lockSalt().get() != null &&
|
||||
prefs.eh_lockLength().getOrDefault() != -1
|
||||
|
||||
/**
|
||||
* Check if the lock will function properly
|
||||
@@ -53,30 +53,35 @@ fun notifyLockSecurity(
|
||||
): Boolean {
|
||||
return false
|
||||
if (!prefs.eh_lockManually().getOrDefault() &&
|
||||
!hasAccessToUsageStats(context)) {
|
||||
!hasAccessToUsageStats(context)
|
||||
) {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Permission required")
|
||||
.message(text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
"This is required for the application lock to function properly. " +
|
||||
"Press OK to grant this permission now.")
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
|
||||
MaterialDialog(context)
|
||||
.title(text = "Grant permission manually")
|
||||
.message(text = "Failed to launch the window used to grant the usage stats permission. " +
|
||||
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'.")
|
||||
.positiveButton(android.R.string.ok) { it.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
}
|
||||
.title(text = "Permission required")
|
||||
.message(
|
||||
text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
"This is required for the application lock to function properly. " +
|
||||
"Press OK to grant this permission now."
|
||||
)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
|
||||
MaterialDialog(context)
|
||||
.title(text = "Grant permission manually")
|
||||
.message(
|
||||
text = "Failed to launch the window used to grant the usage stats permission. " +
|
||||
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'."
|
||||
)
|
||||
.positiveButton(android.R.string.ok) { it.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
||||
@@ -97,10 +97,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
val parsedUrl = Uri.parse(url)
|
||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT))
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
|
||||
}
|
||||
// Check login result
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
}
|
||||
@@ -128,9 +129,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
fun checkLoginCookies(url: String): Boolean {
|
||||
getCookies(url)?.let { parsed ->
|
||||
return parsed.filter {
|
||||
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
|
||||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)) &&
|
||||
it.value.isNotBlank()
|
||||
(
|
||||
it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
|
||||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)
|
||||
) &&
|
||||
it.value.isNotBlank()
|
||||
}.count() >= 2
|
||||
}
|
||||
return false
|
||||
@@ -168,11 +171,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
}
|
||||
|
||||
fun getCookies(url: String): List<HttpCookie>? =
|
||||
CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PARAM_SKIP_INJECT = "TEH_SKIP_INJECT"
|
||||
@@ -181,7 +184,8 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
const val PASS_HASH_COOKIE = "ipb_pass_hash"
|
||||
const val IGNEOUS_COOKIE = "igneous"
|
||||
|
||||
const val HIDE_JS = """
|
||||
const val HIDE_JS =
|
||||
"""
|
||||
javascript:(function () {
|
||||
document.getElementsByTagName('body')[0].style.visibility = 'hidden';
|
||||
document.getElementsByName('submit')[0].style.visibility = 'visible';
|
||||
|
||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: SourceController.SmartSearchConfig?) :
|
||||
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||
|
||||
override val coroutineContext = Job() + Dispatchers.Main
|
||||
|
||||
|
||||
@@ -14,16 +14,16 @@ import java.util.Date
|
||||
|
||||
inline fun <reified E : RealmModel> RealmQuery<out E>.beginLog(
|
||||
clazz: Class<out E>? =
|
||||
E::class.java
|
||||
E::class.java
|
||||
): LoggingRealmQuery<out E> =
|
||||
LoggingRealmQuery.fromQuery(this, clazz)
|
||||
|
||||
class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
companion object {
|
||||
fun <E : RealmModel> fromQuery(q: RealmQuery<out E>, clazz: Class<out E>?) =
|
||||
LoggingRealmQuery(q).apply {
|
||||
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
|
||||
}
|
||||
LoggingRealmQuery(q).apply {
|
||||
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
|
||||
}
|
||||
}
|
||||
|
||||
private val log = mutableListOf<String>()
|
||||
@@ -47,9 +47,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendEqualTo(fieldName: String, value: String, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" == \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" == \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun equalTo(fieldName: String, value: String): RealmQuery<E> {
|
||||
@@ -108,11 +112,18 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
fun appendIn(fieldName: String, values: Array<out Any?>, casing: Case? = null) {
|
||||
log += sec("[${values.joinToString(separator = ", ", transform = {
|
||||
"\"$it\""
|
||||
})}] IN \"$fieldName\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"[${values.joinToString(
|
||||
separator = ", ",
|
||||
transform = {
|
||||
"\"$it\""
|
||||
}
|
||||
)}] IN \"$fieldName\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun `in`(fieldName: String, values: Array<String>): RealmQuery<E> {
|
||||
@@ -166,9 +177,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendNotEqualTo(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" != \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" != \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun notEqualTo(fieldName: String, value: String): RealmQuery<E> {
|
||||
@@ -372,9 +387,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendContains(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" CONTAINS \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" CONTAINS \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun contains(fieldName: String, value: String): RealmQuery<E> {
|
||||
@@ -388,9 +407,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendBeginsWith(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" BEGINS WITH \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" BEGINS WITH \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun beginsWith(fieldName: String, value: String): RealmQuery<E> {
|
||||
@@ -404,9 +427,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendEndsWith(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" ENDS WITH \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" ENDS WITH \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun endsWith(fieldName: String, value: String): RealmQuery<E> {
|
||||
@@ -420,9 +447,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendLike(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" LIKE \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" LIKE \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun like(fieldName: String, value: String): RealmQuery<E> {
|
||||
|
||||
@@ -209,10 +209,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val entries: Set<Map.Entry<String, T>>
|
||||
get() {
|
||||
val out = mutableSetOf<Map.Entry<String, T>>()
|
||||
node.walk("", { k, v ->
|
||||
out.add(AbstractMap.SimpleImmutableEntry(k, v))
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ k, v ->
|
||||
out.add(AbstractMap.SimpleImmutableEntry(k, v))
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
/**
|
||||
@@ -221,10 +225,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val keys: Set<String>
|
||||
get() {
|
||||
val out = mutableSetOf<String>()
|
||||
node.walk("", { k, _ ->
|
||||
out.add(k)
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ k, _ ->
|
||||
out.add(k)
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -243,10 +251,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val values: Collection<T>
|
||||
get() {
|
||||
val out = mutableSetOf<T>()
|
||||
node.walk("", { _, v ->
|
||||
out.add(v)
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ _, v ->
|
||||
out.add(v)
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -264,10 +276,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
* Returns `true` if the map maps one or more keys to the specified [value].
|
||||
*/
|
||||
override fun containsValue(value: T): Boolean {
|
||||
node.walk("", { _, v ->
|
||||
if (v == value) return true
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ _, v ->
|
||||
if (v == value) return true
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -315,32 +331,38 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
* Returns a [MutableSet] of all key/value pairs in this map.
|
||||
*/
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<String, T>>
|
||||
get() = FakeMutableSet.fromSet(mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
|
||||
walk { k, v ->
|
||||
this += FakeMutableEntry.fromPair(k, v)
|
||||
true
|
||||
get() = FakeMutableSet.fromSet(
|
||||
mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
|
||||
walk { k, v ->
|
||||
this += FakeMutableEntry.fromPair(k, v)
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [MutableSet] of all keys in this map.
|
||||
*/
|
||||
override val keys: MutableSet<String>
|
||||
get() = FakeMutableSet.fromSet(mutableSetOf<String>().apply {
|
||||
walk { k, _ ->
|
||||
this += k
|
||||
true
|
||||
get() = FakeMutableSet.fromSet(
|
||||
mutableSetOf<String>().apply {
|
||||
walk { k, _ ->
|
||||
this += k
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [MutableCollection] of all values in this map. Note that this collection may contain duplicate values.
|
||||
*/
|
||||
override val values: MutableCollection<T>
|
||||
get() = FakeMutableCollection.fromCollection(mutableListOf<T>().apply {
|
||||
walk { _, v ->
|
||||
this += v
|
||||
true
|
||||
get() = FakeMutableCollection.fromCollection(
|
||||
mutableListOf<T>().apply {
|
||||
walk { _, v ->
|
||||
this += v
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import org.jsoup.nodes.Document
|
||||
fun Response.interceptAsHtml(block: (Document) -> Unit): Response {
|
||||
val body = body
|
||||
if (body?.contentType()?.type == "text" &&
|
||||
body.contentType()?.subtype == "html") {
|
||||
body.contentType()?.subtype == "html"
|
||||
) {
|
||||
val bodyString = body.string()
|
||||
val rebuiltResponse = newBuilder()
|
||||
.body(ResponseBody.create(body.contentType(), bodyString))
|
||||
.build()
|
||||
.body(ResponseBody.create(body.contentType(), bodyString))
|
||||
.build()
|
||||
try {
|
||||
// Search for captcha
|
||||
val parsed = asJsoup(html = bodyString)
|
||||
|
||||
@@ -53,4 +53,4 @@ fun <T : RealmModel> Realm.createUUIDObj(clazz: Class<T>) =
|
||||
createObject(clazz, UUID.randomUUID().toString())!!
|
||||
|
||||
inline fun <reified T : RealmModel> Realm.createUUIDObj() =
|
||||
createUUIDObj(T::class.java)
|
||||
createUUIDObj(T::class.java)
|
||||
|
||||
@@ -37,14 +37,18 @@ suspend fun <T> Single<T>.await(subscribeOn: Scheduler? = null): T {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
|
||||
lateinit var sub: Subscription
|
||||
sub = self.subscribe({
|
||||
continuation.resume(it) {
|
||||
sub.unsubscribe()
|
||||
sub = self.subscribe(
|
||||
{
|
||||
continuation.resume(it) {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
},
|
||||
{
|
||||
if (!continuation.isCancelled) {
|
||||
continuation.resumeWithException(it)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
if (!continuation.isCancelled)
|
||||
continuation.resumeWithException(it)
|
||||
})
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
sub.unsubscribe()
|
||||
@@ -59,14 +63,18 @@ suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
|
||||
lateinit var sub: Subscription
|
||||
sub = self.subscribe({
|
||||
continuation.resume(Unit) {
|
||||
sub.unsubscribe()
|
||||
sub = self.subscribe(
|
||||
{
|
||||
continuation.resume(Unit) {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
},
|
||||
{
|
||||
if (!continuation.isCancelled) {
|
||||
continuation.resumeWithException(it)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
if (!continuation.isCancelled)
|
||||
continuation.resumeWithException(it)
|
||||
})
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
sub.unsubscribe()
|
||||
|
||||
@@ -14,15 +14,21 @@ private val galleryAdder by lazy {
|
||||
* A version of fetchSearchManga that supports URL importing
|
||||
*/
|
||||
fun UrlImportableSource.urlImportFetchSearchManga(query: String, fail: () -> Observable<MangasPage>) =
|
||||
when {
|
||||
query.startsWith("http://") || query.startsWith("https://") -> {
|
||||
Observable.fromCallable {
|
||||
val res = galleryAdder.addGallery(query, false, this)
|
||||
MangasPage((if (res is GalleryAddEvent.Success)
|
||||
listOf(res.manga)
|
||||
else
|
||||
emptyList()), false)
|
||||
}
|
||||
when {
|
||||
query.startsWith("http://") || query.startsWith("https://") -> {
|
||||
Observable.fromCallable {
|
||||
val res = galleryAdder.addGallery(query, false, this)
|
||||
MangasPage(
|
||||
(
|
||||
if (res is GalleryAddEvent.Success) {
|
||||
listOf(res.manga)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
),
|
||||
false
|
||||
)
|
||||
}
|
||||
else -> fail()
|
||||
}
|
||||
else -> fail()
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ class SparseArrayCollection<E>(val sparseArray: SparseArray<E>, var reverse: Boo
|
||||
var idx = index++
|
||||
if (reverse) idx = sparseArray.size() - 1 - idx
|
||||
return AbstractMap.SimpleImmutableEntry(
|
||||
sparseArray.keyAt(idx),
|
||||
sparseArray.valueAt(idx)
|
||||
sparseArray.keyAt(idx),
|
||||
sparseArray.valueAt(idx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user