Linting Fixes AZ

This commit is contained in:
Jobobby04
2020-05-02 00:46:24 -04:00
parent 03e5c5ca10
commit 7e99a9f789
108 changed files with 2962 additions and 2412 deletions
+11 -11
View File
@@ -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
+51 -35
View File
@@ -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
+18 -16
View File
@@ -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")
}
}
+30 -21
View File
@@ -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()
}
+76 -73
View File
@@ -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)
)
}
}
+65 -51
View File
@@ -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
+57 -47
View File
@@ -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"
}
+1 -1
View File
@@ -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 {
+18 -17
View File
@@ -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,
+54 -53
View File
@@ -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"
+11 -9
View File
@@ -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()
)
}
}
+25 -18
View File
@@ -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("%", "\\%")
}
}
}
+10 -10
View File
@@ -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) {
+36 -22
View File
@@ -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())
}
+26 -23
View File
@@ -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
}
+23 -23
View File
@@ -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()
}
+29 -24
View File
@@ -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
+58 -27
View File
@@ -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> {
+53 -31
View File
@@ -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
}
}
})
)
}
+4 -3
View File
@@ -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)
+1 -1
View File
@@ -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)
+22 -14
View File
@@ -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()
+16 -10
View File
@@ -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)
)
}
}