Linting Fixes AZ
This commit is contained in:
parent
03e5c5ca10
commit
7e99a9f789
@ -83,10 +83,12 @@ class ChapterCache(private val context: Context) {
|
||||
// --> EH
|
||||
// Cache size is in MB
|
||||
private fun setupDiskCache(cacheSize: Long): DiskLruCache {
|
||||
return DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
cacheSize * 1024 * 1024)
|
||||
return DiskLruCache.open(
|
||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
cacheSize * 1024 * 1024
|
||||
)
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
|
@ -19,18 +19,22 @@ interface ChapterQueries : DbProvider {
|
||||
|
||||
fun getChaptersByMangaId(mangaId: Long?) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getMergedChaptersQuery(mangaId))
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getRecentChapters(date: Date) = db.get()
|
||||
@ -80,11 +84,13 @@ interface ChapterQueries : DbProvider {
|
||||
|
||||
fun getChapters(url: String) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(url)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
|
||||
|
@ -10,7 +10,8 @@ import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged
|
||||
/**
|
||||
* Query to get the manga merged into a merged manga
|
||||
*/
|
||||
fun getMergedMangaQuery(id: Long) = """
|
||||
fun getMergedMangaQuery(id: Long) =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
||||
@ -22,7 +23,8 @@ fun getMergedMangaQuery(id: Long) = """
|
||||
/**
|
||||
* Query to get the chapters of all manga in a merged manga
|
||||
*/
|
||||
fun getMergedChaptersQuery(id: Long) = """
|
||||
fun getMergedChaptersQuery(id: Long) =
|
||||
"""
|
||||
SELECT ${Chapter.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
||||
|
@ -21,10 +21,10 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_URL, manga.url)
|
||||
|
@ -9,7 +9,8 @@ object MergedTable {
|
||||
const val COL_MANGA_ID = "mangaID"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_MERGE_ID INTEGER NOT NULL,
|
||||
$COL_MANGA_ID INTEGER NOT NULL
|
||||
)"""
|
||||
|
@ -147,7 +147,7 @@ class ExtensionManager(
|
||||
|
||||
fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean =
|
||||
preferences.eh_enableSourceBlacklist().get()
|
||||
preferences.eh_enableSourceBlacklist().get()
|
||||
): Boolean {
|
||||
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
private fun newMetaInstance() = metaClass.constructors.find {
|
||||
it.parameters.isEmpty()
|
||||
}?.call()
|
||||
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
|
||||
/**
|
||||
* Parses metadata from the input and then copies it into the manga
|
||||
|
@ -19,10 +19,12 @@ interface UrlImportableSource : Source {
|
||||
return try {
|
||||
val uri = URI(url)
|
||||
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) {
|
||||
url
|
||||
|
@ -73,16 +73,18 @@ class EHentai(
|
||||
override val metaClass = EHentaiSearchMetadata::class
|
||||
|
||||
val schema: String
|
||||
get() = if (prefs.secureEXH().getOrDefault())
|
||||
get() = if (prefs.secureEXH().getOrDefault()) {
|
||||
"https"
|
||||
else
|
||||
} else {
|
||||
"http"
|
||||
}
|
||||
|
||||
val domain: String
|
||||
get() = if (exh)
|
||||
get() = if (exh) {
|
||||
"exhentai.org"
|
||||
else
|
||||
} else {
|
||||
"e-hentai.org"
|
||||
}
|
||||
|
||||
override val baseUrl: String
|
||||
get() = "$schema://$domain"
|
||||
@ -111,25 +113,27 @@ class EHentai(
|
||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||
|
||||
ParsedManga(
|
||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||
favElement?.attr("style")?.substring(14, 17)
|
||||
),
|
||||
manga = Manga.create(id).apply {
|
||||
// Get title
|
||||
title = thumbnailElement.attr("title")
|
||||
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
||||
// Get image
|
||||
thumbnail_url = thumbnailElement.attr("src")
|
||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||
favElement?.attr("style")?.substring(14, 17)
|
||||
),
|
||||
manga = Manga.create(id).apply {
|
||||
// Get title
|
||||
title = thumbnailElement.attr("title")
|
||||
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
||||
// Get image
|
||||
thumbnail_url = thumbnailElement.attr("src")
|
||||
|
||||
// TODO Parse genre + uploader + tags
|
||||
})
|
||||
// TODO Parse genre + uploader + tags
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val parsedLocation = doc.location().toHttpUrlOrNull()
|
||||
|
||||
// Add to page if required
|
||||
val hasNextPage = if (parsedLocation == null ||
|
||||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)
|
||||
) {
|
||||
select("a[onclick=return false]").last()?.let {
|
||||
it.text() == ">"
|
||||
} ?: false
|
||||
@ -160,7 +164,7 @@ class EHentai(
|
||||
while (true) {
|
||||
val gid = EHentaiSearchMetadata.galleryId(url).toInt()
|
||||
val cachedParent = updateHelper.parentLookupTable.get(
|
||||
gid
|
||||
gid
|
||||
)
|
||||
if (cachedParent == null) {
|
||||
throttleFunc()
|
||||
@ -175,19 +179,19 @@ class EHentai(
|
||||
|
||||
if (parentLink != null) {
|
||||
updateHelper.parentLookupTable.put(
|
||||
gid,
|
||||
GalleryEntry(
|
||||
EHentaiSearchMetadata.galleryId(parentLink),
|
||||
EHentaiSearchMetadata.galleryToken(parentLink)
|
||||
)
|
||||
gid,
|
||||
GalleryEntry(
|
||||
EHentaiSearchMetadata.galleryId(parentLink),
|
||||
EHentaiSearchMetadata.galleryToken(parentLink)
|
||||
)
|
||||
)
|
||||
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
||||
} else break
|
||||
} else {
|
||||
XLog.d("Parent cache hit: %s!", gid)
|
||||
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
||||
cachedParent.gId,
|
||||
cachedParent.gToken
|
||||
cachedParent.gId,
|
||||
cachedParent.gToken
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -201,9 +205,11 @@ class EHentai(
|
||||
url = EHentaiSearchMetadata.normalizeUrl(d.location())
|
||||
name = "v1: " + d.selectFirst("#gn").text()
|
||||
chapter_number = 1f
|
||||
date_upload = EX_DATE_FORMAT.parse(d.select("#gdd .gdt1").find { el ->
|
||||
el.text().toLowerCase() == "posted:"
|
||||
}!!.nextElementSibling().text()).time
|
||||
date_upload = EX_DATE_FORMAT.parse(
|
||||
d.select("#gdd .gdt1").find { el ->
|
||||
el.text().toLowerCase() == "posted:"
|
||||
}!!.nextElementSibling().text()
|
||||
).time
|
||||
}
|
||||
// Build and append the rest of the galleries
|
||||
if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self)
|
||||
@ -253,27 +259,32 @@ class EHentai(
|
||||
}.sortedBy(Pair<Int, String>::first).map { it.second }
|
||||
}
|
||||
|
||||
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
|
||||
private fun chapterPageRequest(np: String) = exGet(np, null, headers)
|
||||
private fun chapterPageCall(np: String): Observable<Response> {
|
||||
return client.newCall(chapterPageRequest(np)).asObservableSuccess()
|
||||
}
|
||||
private fun chapterPageRequest(np: String): Request {
|
||||
return exGet(np, null, headers)
|
||||
}
|
||||
|
||||
private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let {
|
||||
return if (it.text() == ">") it.attr("href") else null
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = if (exh)
|
||||
override fun popularMangaRequest(page: Int) = if (exh) {
|
||||
latestUpdatesRequest(page)
|
||||
else
|
||||
} else {
|
||||
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
|
||||
}
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
urlImportFetchSearchManga(query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
|
||||
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
|
||||
@ -287,20 +298,20 @@ class EHentai(
|
||||
// Reverse search results on filter
|
||||
if (filters.any { it is ReverseFilter && it.state }) {
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val doc = it.asJsoup()
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val doc = it.asJsoup()
|
||||
|
||||
val elements = doc.select(".ptt > tbody > tr > td")
|
||||
val elements = doc.select(".ptt > tbody > tr > td")
|
||||
|
||||
val totalElement = elements[elements.size - 2]
|
||||
val totalElement = elements[elements.size - 2]
|
||||
|
||||
val thisPage = totalElement.text().toInt() - (page - 1)
|
||||
val thisPage = totalElement.text().toInt() - (page - 1)
|
||||
|
||||
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
|
||||
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
|
||||
|
||||
exGet(uri.toString(), thisPage)
|
||||
}
|
||||
exGet(uri.toString(), thisPage)
|
||||
}
|
||||
} else {
|
||||
return Observable.just(request)
|
||||
}
|
||||
@ -314,22 +325,28 @@ class EHentai(
|
||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
|
||||
|
||||
fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true) = GET(page?.let {
|
||||
addParam(url, "page", Integer.toString(page - 1))
|
||||
} ?: url, additionalHeaders?.let {
|
||||
val headers = headers.newBuilder()
|
||||
it.toMultimap().forEach { (t, u) ->
|
||||
u.forEach {
|
||||
headers.add(t, it)
|
||||
fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
|
||||
return GET(
|
||||
page?.let {
|
||||
addParam(url, "page", Integer.toString(page - 1))
|
||||
} ?: url,
|
||||
additionalHeaders?.let {
|
||||
val headers = headers.newBuilder()
|
||||
it.toMultimap().forEach { (t, u) ->
|
||||
u.forEach {
|
||||
headers.add(t, it)
|
||||
}
|
||||
}
|
||||
headers.build()
|
||||
} ?: headers
|
||||
).let {
|
||||
if (!cache) {
|
||||
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
headers.build()
|
||||
} ?: headers).let {
|
||||
if (!cache)
|
||||
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
||||
else
|
||||
it
|
||||
}!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
@ -339,33 +356,37 @@ class EHentai(
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableWithAsyncStacktrace()
|
||||
.flatMap { (stacktrace, response) ->
|
||||
if (response.isSuccessful) {
|
||||
// Pull to most recent
|
||||
val doc = response.asJsoup()
|
||||
val newerGallery = doc.select("#gnd a").lastOrNull()
|
||||
val pre = if (newerGallery != null && DebugToggles.PULL_TO_ROOT_WHEN_LOADING_EXH_MANGA_DETAILS.enabled) {
|
||||
manga.url = EHentaiSearchMetadata.normalizeUrl(newerGallery.attr("href"))
|
||||
client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess().map { it.asJsoup() }
|
||||
} else Observable.just(doc)
|
||||
.asObservableWithAsyncStacktrace()
|
||||
.flatMap { (stacktrace, response) ->
|
||||
if (response.isSuccessful) {
|
||||
// Pull to most recent
|
||||
val doc = response.asJsoup()
|
||||
val newerGallery = doc.select("#gnd a").lastOrNull()
|
||||
val pre = if (newerGallery != null && DebugToggles.PULL_TO_ROOT_WHEN_LOADING_EXH_MANGA_DETAILS.enabled) {
|
||||
manga.url = EHentaiSearchMetadata.normalizeUrl(newerGallery.attr("href"))
|
||||
client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess().map { it.asJsoup() }
|
||||
} else Observable.just(doc)
|
||||
|
||||
pre.flatMap {
|
||||
parseToManga(manga, it).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
pre.flatMap {
|
||||
parseToManga(manga, it).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
response.close()
|
||||
|
||||
if (response.code == 404) {
|
||||
throw GalleryNotFoundException(stacktrace)
|
||||
} else {
|
||||
response.close()
|
||||
|
||||
if (response.code == 404) {
|
||||
throw GalleryNotFoundException(stacktrace)
|
||||
} else {
|
||||
throw Exception("HTTP error ${response.code}", stacktrace)
|
||||
}
|
||||
throw Exception("HTTP error ${response.code}", stacktrace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,11 +410,11 @@ class EHentai(
|
||||
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
|
||||
}
|
||||
genre = select(".cs")
|
||||
.attr("onclick")
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'")
|
||||
.attr("onclick")
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'")
|
||||
|
||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||
|
||||
@ -404,8 +425,10 @@ class EHentai(
|
||||
val right = rightElement.text().nullIfBlank()?.trim()
|
||||
if (left != null && right != null) {
|
||||
ignore {
|
||||
when (left.removeSuffix(":")
|
||||
.toLowerCase()) {
|
||||
when (
|
||||
left.removeSuffix(":")
|
||||
.toLowerCase()
|
||||
) {
|
||||
"posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
|
||||
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
|
||||
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
|
||||
@ -428,7 +451,8 @@ class EHentai(
|
||||
|
||||
lastUpdateCheck = System.currentTimeMillis()
|
||||
if (datePosted != null &&
|
||||
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
|
||||
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
|
||||
) {
|
||||
aged = true
|
||||
XLog.d("aged %s - too old", title)
|
||||
}
|
||||
@ -436,32 +460,35 @@ class EHentai(
|
||||
// Parse ratings
|
||||
ignore {
|
||||
averageRating = select("#rating_label")
|
||||
.text()
|
||||
.removePrefix("Average:")
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toDouble()
|
||||
.text()
|
||||
.removePrefix("Average:")
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toDouble()
|
||||
ratingCount = select("#rating_count")
|
||||
.text()
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toInt()
|
||||
.text()
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toInt()
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
tags.clear()
|
||||
select("#taglist tr").forEach {
|
||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||
tags.addAll(it.select("div").map { element ->
|
||||
RaisedTag(
|
||||
tags.addAll(
|
||||
it.select("div").map { element ->
|
||||
RaisedTag(
|
||||
namespace,
|
||||
element.text().trim(),
|
||||
if (element.hasClass("gtl"))
|
||||
if (element.hasClass("gtl")) {
|
||||
TAG_TYPE_LIGHT
|
||||
else
|
||||
} else {
|
||||
TAG_TYPE_NORMAL
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Add genre as virtual tag
|
||||
@ -478,8 +505,8 @@ class EHentai(
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { realImageUrlParse(it, page) }
|
||||
.asObservableSuccess()
|
||||
.map { realImageUrlParse(it, page) }
|
||||
}
|
||||
|
||||
fun realImageUrlParse(response: Response, page: Page): String {
|
||||
@ -505,9 +532,13 @@ class EHentai(
|
||||
var favNames: List<String>? = null
|
||||
|
||||
do {
|
||||
val response2 = client.newCall(exGet(favoriteUrl,
|
||||
val response2 = client.newCall(
|
||||
exGet(
|
||||
favoriteUrl,
|
||||
page = page,
|
||||
cache = false)).execute()
|
||||
cache = false
|
||||
)
|
||||
).execute()
|
||||
val doc = response2.asJsoup()
|
||||
|
||||
// Parse favorites
|
||||
@ -515,22 +546,24 @@ class EHentai(
|
||||
result += parsed.first
|
||||
|
||||
// Parse fav names
|
||||
if (favNames == null)
|
||||
if (favNames == null) {
|
||||
favNames = doc.select(".fp:not(.fps)").mapNotNull {
|
||||
it.child(2).text()
|
||||
}
|
||||
|
||||
}
|
||||
// Next page
|
||||
|
||||
page++
|
||||
} while (parsed.second)
|
||||
|
||||
return Pair(result as List<ParsedManga>, favNames!!)
|
||||
}
|
||||
|
||||
fun spPref() = if (exh)
|
||||
fun spPref() = if (exh) {
|
||||
prefs.eh_exhSettingsProfile()
|
||||
else
|
||||
} else {
|
||||
prefs.eh_ehSettingsProfile()
|
||||
}
|
||||
|
||||
fun rawCookies(sp: Int): Map<String, String> {
|
||||
val cookies: MutableMap<String, String> = mutableMapOf()
|
||||
@ -541,16 +574,19 @@ class EHentai(
|
||||
cookies["sp"] = sp.toString()
|
||||
|
||||
val sessionKey = prefs.eh_settingsKey().getOrDefault()
|
||||
if (sessionKey != null)
|
||||
if (sessionKey != null) {
|
||||
cookies["sk"] = sessionKey
|
||||
}
|
||||
|
||||
val sessionCookie = prefs.eh_sessionCookie().getOrDefault()
|
||||
if (sessionCookie != null)
|
||||
if (sessionCookie != null) {
|
||||
cookies["s"] = sessionCookie
|
||||
}
|
||||
|
||||
val hathPerksCookie = prefs.eh_hathPerksCookies().getOrDefault()
|
||||
if (hathPerksCookie != null)
|
||||
if (hathPerksCookie != null) {
|
||||
cookies["hath_perks"] = hathPerksCookie
|
||||
}
|
||||
}
|
||||
|
||||
// Session-less extended display mode (for users without ExHentai)
|
||||
@ -568,51 +604,57 @@ class EHentai(
|
||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
|
||||
|
||||
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||
.buildUpon()
|
||||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
.buildUpon()
|
||||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.addInterceptor { chain ->
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.removeHeader("Cookie")
|
||||
.addHeader("Cookie", cookiesHeader())
|
||||
.build()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.addInterceptor { chain ->
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.removeHeader("Cookie")
|
||||
.addHeader("Cookie", cookiesHeader())
|
||||
.build()
|
||||
|
||||
chain.proceed(newReq)
|
||||
}.build()!!
|
||||
chain.proceed(newReq)
|
||||
}.build()!!
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
AdvancedGroup(),
|
||||
ReverseFilter()
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
AdvancedGroup(),
|
||||
ReverseFilter()
|
||||
)
|
||||
|
||||
class Watched : Filter.CheckBox("Watched List"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
if (state) {
|
||||
builder.appendPath("watched")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenreOption(name: String, val genreId: Int) : Filter.CheckBox(name, false)
|
||||
class GenreGroup : Filter.Group<GenreOption>("Genres", listOf(
|
||||
GenreOption("Dōjinshi", 2),
|
||||
GenreOption("Manga", 4),
|
||||
GenreOption("Artist CG", 8),
|
||||
GenreOption("Game CG", 16),
|
||||
GenreOption("Western", 512),
|
||||
GenreOption("Non-H", 256),
|
||||
GenreOption("Image Set", 32),
|
||||
GenreOption("Cosplay", 64),
|
||||
GenreOption("Asian Porn", 128),
|
||||
GenreOption("Misc", 1)
|
||||
)), UriFilter {
|
||||
class GenreGroup :
|
||||
Filter.Group<GenreOption>(
|
||||
"Genres",
|
||||
listOf(
|
||||
GenreOption("Dōjinshi", 2),
|
||||
GenreOption("Manga", 4),
|
||||
GenreOption("Artist CG", 8),
|
||||
GenreOption("Game CG", 16),
|
||||
GenreOption("Western", 512),
|
||||
GenreOption("Non-H", 256),
|
||||
GenreOption("Image Set", 32),
|
||||
GenreOption("Cosplay", 64),
|
||||
GenreOption("Asian Porn", 128),
|
||||
GenreOption("Misc", 1)
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
val bits = state.fold(0) { acc, genre ->
|
||||
if (!genre.state) acc + genre.genreId else acc
|
||||
@ -623,8 +665,9 @@ class EHentai(
|
||||
|
||||
class AdvancedOption(name: String, val param: String, defValue: Boolean = false) : Filter.CheckBox(name, defValue), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
if (state) {
|
||||
builder.appendQueryParameter(param, "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -643,13 +686,18 @@ class EHentai(
|
||||
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
|
||||
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
|
||||
|
||||
class RatingOption : Filter.Select<String>("Minimum Rating", arrayOf(
|
||||
"Any",
|
||||
"2 stars",
|
||||
"3 stars",
|
||||
"4 stars",
|
||||
"5 stars"
|
||||
)), UriFilter {
|
||||
class RatingOption :
|
||||
Filter.Select<String>(
|
||||
"Minimum Rating",
|
||||
arrayOf(
|
||||
"Any",
|
||||
"2 stars",
|
||||
"3 stars",
|
||||
"4 stars",
|
||||
"5 stars"
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state > 0) {
|
||||
builder.appendQueryParameter("f_srdd", Integer.toString(state + 1))
|
||||
@ -658,7 +706,9 @@ class EHentai(
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancedGroup : UriGroup<Filter<*>>("Advanced Options", listOf(
|
||||
class AdvancedGroup : UriGroup<Filter<*>>(
|
||||
"Advanced Options",
|
||||
listOf(
|
||||
AdvancedOption("Search Gallery Name", "f_sname", true),
|
||||
AdvancedOption("Search Gallery Tags", "f_stags", true),
|
||||
AdvancedOption("Search Gallery Description", "f_sdesc"),
|
||||
@ -670,24 +720,26 @@ class EHentai(
|
||||
RatingOption(),
|
||||
MinPagesOption(),
|
||||
MaxPagesOption()
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
class ReverseFilter : Filter.CheckBox("Reverse search results")
|
||||
|
||||
override val name = if (exh)
|
||||
override val name = if (exh) {
|
||||
"ExHentai"
|
||||
else
|
||||
} else {
|
||||
"E-Hentai"
|
||||
}
|
||||
|
||||
class GalleryNotFoundException(cause: Throwable) : RuntimeException("Gallery not found!", cause)
|
||||
|
||||
// === URL IMPORT STUFF
|
||||
|
||||
override val matchingHosts: List<String> = if (exh) listOf(
|
||||
"exhentai.org"
|
||||
"exhentai.org"
|
||||
) else listOf(
|
||||
"g.e-hentai.org",
|
||||
"e-hentai.org"
|
||||
"g.e-hentai.org",
|
||||
"e-hentai.org"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
@ -717,17 +769,23 @@ class EHentai(
|
||||
val json = JsonObject()
|
||||
json["method"] = "gtoken"
|
||||
json["pagelist"] = JsonArray().apply {
|
||||
add(JsonArray().apply {
|
||||
add(gallery.toInt())
|
||||
add(pageToken)
|
||||
add(pageNum.toInt())
|
||||
})
|
||||
add(
|
||||
JsonArray().apply {
|
||||
add(gallery.toInt())
|
||||
add(pageToken)
|
||||
add(pageNum.toInt())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val outJson = JsonParser.parseString(client.newCall(Request.Builder()
|
||||
.url(EH_API_BASE)
|
||||
.post(RequestBody.create(JSON, json.toString()))
|
||||
.build()).execute().body!!.string()).obj
|
||||
val outJson = JsonParser.parseString(
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url(EH_API_BASE)
|
||||
.post(RequestBody.create(JSON, json.toString()))
|
||||
.build()
|
||||
).execute().body!!.string()
|
||||
).obj
|
||||
|
||||
val obj = outJson["tokenlist"].array.first()
|
||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||
@ -742,16 +800,16 @@ class EHentai(
|
||||
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||
|
||||
private val FAVORITES_BORDER_HEX_COLORS = listOf(
|
||||
"000",
|
||||
"f00",
|
||||
"fa0",
|
||||
"dd0",
|
||||
"080",
|
||||
"9f4",
|
||||
"4bf",
|
||||
"00f",
|
||||
"508",
|
||||
"e8e"
|
||||
"000",
|
||||
"f00",
|
||||
"fa0",
|
||||
"dd0",
|
||||
"080",
|
||||
"9f4",
|
||||
"4bf",
|
||||
"00f",
|
||||
"508",
|
||||
"e8e"
|
||||
)
|
||||
|
||||
fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") {
|
||||
|
@ -65,7 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
private fun tagIndexVersion(): Single<Long> {
|
||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
||||
return if (sCachedTagIndexVersion == null ||
|
||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedTagIndexVersion = it
|
||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
||||
@ -80,7 +81,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
private fun galleryIndexVersion(): Single<Long> {
|
||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
||||
return if (sCachedGalleryIndexVersion == null ||
|
||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedGalleryIndexVersion = it
|
||||
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
||||
@ -162,9 +164,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
|
||||
"$LTN_BASE_URL/popular-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
"$LTN_BASE_URL/popular-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
)
|
||||
|
||||
/**
|
||||
@ -192,7 +194,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
|
||||
// TODO Cache the results coming out of HitomiNozomi
|
||||
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
|
||||
.map { HitomiNozomi(client, it.first, it.second) }
|
||||
.map { HitomiNozomi(client, it.first, it.second) }
|
||||
|
||||
var base = if (positive.isEmpty()) {
|
||||
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
|
||||
@ -240,9 +242,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
|
||||
"$LTN_BASE_URL/index-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
"$LTN_BASE_URL/index-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
)
|
||||
|
||||
/**
|
||||
@ -254,14 +256,14 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
}
|
||||
|
||||
fun responseToMangas(response: Response): Observable<MangasPage> {
|
||||
@ -270,9 +272,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
val end = range.substringBefore('/').substringAfter('-').toLong()
|
||||
val body = response.body!!
|
||||
return parseNozomiPage(body.bytes())
|
||||
.map {
|
||||
MangasPage(it, end < total - 1)
|
||||
}
|
||||
.map {
|
||||
MangasPage(it, end < total - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
|
||||
@ -285,13 +287,15 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
}
|
||||
|
||||
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
|
||||
return Single.zip(ids.map {
|
||||
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
|
||||
return Single.zip(
|
||||
ids.map {
|
||||
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
|
||||
.asObservableSuccess()
|
||||
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
|
||||
.map { parseGalleryBlock(it) }
|
||||
.toSingle()
|
||||
}) { it.map { m -> m as SManga } }
|
||||
}
|
||||
) { it.map { m -> m as SManga } }
|
||||
}
|
||||
|
||||
private fun parseGalleryBlock(response: Response): SManga {
|
||||
@ -318,23 +322,27 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
)
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -372,9 +380,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
val hashPath1 = hash.takeLast(1)
|
||||
val hashPath2 = hash.takeLast(3).take(2)
|
||||
Page(
|
||||
index,
|
||||
"",
|
||||
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
|
||||
index,
|
||||
"",
|
||||
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -396,19 +404,20 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
it[it.lastIndex - 1]
|
||||
}
|
||||
return request.newBuilder()
|
||||
.header("Referer", "$BASE_URL/reader/$hlId.html")
|
||||
.build()
|
||||
.header("Referer", "$BASE_URL/reader/$hlId.html")
|
||||
.build()
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"hitomi.la"
|
||||
"hitomi.la"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
|
||||
if (lcFirstPathSegment != "manga" && lcFirstPathSegment != "reader")
|
||||
if (lcFirstPathSegment != "manga" && lcFirstPathSegment != "reader") {
|
||||
return null
|
||||
}
|
||||
|
||||
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
||||
}
|
||||
@ -419,10 +428,11 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
private val NUMBER_OF_FRONTENDS = 2
|
||||
|
||||
private val DATE_FORMAT by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
|
||||
else
|
||||
} else {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
}
|
||||
|
||||
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()?.state
|
||||
?: defaultSortFilterSelection()
|
||||
?: defaultSortFilterSelection()
|
||||
|
||||
if (sortFilter.index == 1) {
|
||||
if (query.isBlank()) error("You must specify a search query if you wish to sort by popularity!")
|
||||
@ -89,22 +89,22 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
|
||||
if (sortFilter.ascending) {
|
||||
return client.newCall(nhGet(uri.toString()))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val doc = it.asJsoup()
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val doc = it.asJsoup()
|
||||
|
||||
val lastPage = doc.selectFirst(".last")
|
||||
?.attr("href")
|
||||
?.substringAfterLast('=')
|
||||
?.toIntOrNull() ?: 1
|
||||
val lastPage = doc.selectFirst(".last")
|
||||
?.attr("href")
|
||||
?.substringAfterLast('=')
|
||||
?.toIntOrNull() ?: 1
|
||||
|
||||
val thisPage = lastPage - (page - 1)
|
||||
val thisPage = lastPage - (page - 1)
|
||||
|
||||
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
|
||||
uri.appendQueryParameter("page", thisPage.toString())
|
||||
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
|
||||
uri.appendQueryParameter("page", thisPage.toString())
|
||||
|
||||
nhGet(uri.toString(), page)
|
||||
}
|
||||
nhGet(uri.toString(), page)
|
||||
}
|
||||
}
|
||||
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
@ -134,12 +134,16 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url)
|
||||
@ -208,31 +212,38 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
}?.apply {
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
if (it.first != null && it.second != null)
|
||||
if (it.first != null && it.second != null) {
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) {
|
||||
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga) = Observable.just(listOf(SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}))
|
||||
override fun fetchChapterList(manga: SManga) = Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override fun fetchPageList(chapter: SChapter) = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
|
||||
if (metadata.mediaId == null) emptyList()
|
||||
else
|
||||
if (metadata.mediaId == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
metadata.pageImageTypes.mapIndexed { index, s ->
|
||||
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
|
||||
Page(index, imageUrl!!, imageUrl)
|
||||
}
|
||||
}
|
||||
}.toObservable()
|
||||
|
||||
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
|
||||
@ -259,9 +270,9 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
||||
|
||||
class SortFilter : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("Date", "Popular"),
|
||||
defaultSortFilterSelection()
|
||||
"Sort",
|
||||
arrayOf("Date", "Popular"),
|
||||
defaultSortFilterSelection()
|
||||
)
|
||||
|
||||
val appName by lazy {
|
||||
@ -269,14 +280,16 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
}
|
||||
|
||||
fun nhGet(url: String, tag: Any? = null) = GET(url)
|
||||
.newBuilder()
|
||||
.header("User-Agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/56.0.2924.87 " +
|
||||
"Safari/537.36 " +
|
||||
"$appName/${BuildConfig.VERSION_CODE}")
|
||||
.tag(tag).build()
|
||||
.newBuilder()
|
||||
.header(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/56.0.2924.87 " +
|
||||
"Safari/537.36 " +
|
||||
"$appName/${BuildConfig.VERSION_CODE}"
|
||||
)
|
||||
.tag(tag).build()
|
||||
|
||||
override val id = NHENTAI_SOURCE_ID
|
||||
|
||||
@ -291,12 +304,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
// === URL IMPORT STUFF
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"nhentai.net"
|
||||
"nhentai.net"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
if (uri.pathSegments.firstOrNull()?.toLowerCase() != "g")
|
||||
if (uri.pathSegments.firstOrNull()?.toLowerCase() != "g") {
|
||||
return null
|
||||
}
|
||||
|
||||
return "$baseUrl/g/${uri.pathSegments[1]}/"
|
||||
}
|
||||
@ -308,10 +322,10 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
private fun defaultSortFilterSelection() = Filter.Sort.Selection(0, false)
|
||||
|
||||
private val SOURCE_LANG_LIST = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("English", " english"),
|
||||
Pair("Japanese", " japanese"),
|
||||
Pair("Chinese", " chinese")
|
||||
Pair("All", ""),
|
||||
Pair("English", " english"),
|
||||
Pair("Japanese", " japanese"),
|
||||
Pair("Chinese", " chinese")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,10 @@ import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
|
||||
// TODO Transform into delegated source
|
||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(),
|
||||
LewdSource<PervEdenSearchMetadata, Document>, UrlImportableSource {
|
||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
ParsedHttpSource(),
|
||||
LewdSource<PervEdenSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
@ -62,9 +64,9 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#mangaList > tbody > tr"
|
||||
|
||||
@ -79,9 +81,11 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
override fun searchMangaNextPageSelector() = ".next"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val urlLang = if (lang == "en")
|
||||
val urlLang = if (lang == "en") {
|
||||
"eng"
|
||||
else "it"
|
||||
} else {
|
||||
"it"
|
||||
}
|
||||
return GET("$baseUrl/$urlLang/")
|
||||
}
|
||||
|
||||
@ -129,12 +133,16 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,8 +173,9 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
"Alternative name(s)" -> {
|
||||
if (it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if (!text.isBlank())
|
||||
if (!text.isBlank()) {
|
||||
newAltTitles += text
|
||||
}
|
||||
}
|
||||
}
|
||||
"Artist" -> {
|
||||
@ -176,21 +185,24 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
}
|
||||
}
|
||||
"Genres" -> {
|
||||
if (it is Element && it.tagName() == "a")
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT)
|
||||
}
|
||||
}
|
||||
"Type" -> {
|
||||
if (it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if (!text.isBlank())
|
||||
if (!text.isBlank()) {
|
||||
type = text
|
||||
}
|
||||
}
|
||||
}
|
||||
"Status" -> {
|
||||
if (it is TextNode) {
|
||||
val text = it.text().trim()
|
||||
if (!text.isBlank())
|
||||
if (!text.isBlank()) {
|
||||
status = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -224,10 +236,11 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
name = "Chapter " + linkElement.getElementsByTag("b").text()
|
||||
|
||||
ChapterRecognition.parseChapterNumber(
|
||||
this,
|
||||
SManga.create().apply {
|
||||
title = ""
|
||||
})
|
||||
this,
|
||||
SManga.create().apply {
|
||||
title = ""
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
|
||||
@ -242,37 +255,49 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!!
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
TypeFilterGroup(),
|
||||
ReleaseYearGroup(),
|
||||
StatusFilterGroup()
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
TypeFilterGroup(),
|
||||
ReleaseYearGroup(),
|
||||
StatusFilterGroup()
|
||||
)
|
||||
|
||||
class StatusFilterGroup : UriGroup<StatusFilter>("Status", listOf(
|
||||
class StatusFilterGroup : UriGroup<StatusFilter>(
|
||||
"Status",
|
||||
listOf(
|
||||
StatusFilter("Ongoing", 1),
|
||||
StatusFilter("Completed", 2),
|
||||
StatusFilter("Suspended", 0)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
if (state) {
|
||||
builder.appendQueryParameter("status", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit type arg for listOf() to workaround this: KT-16570
|
||||
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf(
|
||||
class ReleaseYearGroup : UriGroup<Filter<*>>(
|
||||
"Release Year",
|
||||
listOf(
|
||||
ReleaseYearRangeFilter(),
|
||||
ReleaseYearYearFilter()
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
class ReleaseYearRangeFilter : Filter.Select<String>("Range", arrayOf(
|
||||
"on",
|
||||
"after",
|
||||
"before"
|
||||
)), UriFilter {
|
||||
class ReleaseYearRangeFilter :
|
||||
Filter.Select<String>(
|
||||
"Range",
|
||||
arrayOf(
|
||||
"on",
|
||||
"after",
|
||||
"before"
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("releasedType", state.toString())
|
||||
}
|
||||
@ -296,18 +321,22 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilterGroup : UriGroup<TypeFilter>("Type", listOf(
|
||||
class TypeFilterGroup : UriGroup<TypeFilter>(
|
||||
"Type",
|
||||
listOf(
|
||||
TypeFilter("Japanese Manga", 0),
|
||||
TypeFilter("Korean Manhwa", 1),
|
||||
TypeFilter("Chinese Manhua", 2),
|
||||
TypeFilter("Comic", 3),
|
||||
TypeFilter("Doujinshi", 4)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
if (state) {
|
||||
builder.appendQueryParameter("type", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,9 +41,10 @@ import rx.schedulers.Schedulers
|
||||
|
||||
typealias SiteMap = NakedTrie<Unit>
|
||||
|
||||
class EightMuses : HttpSource(),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
class EightMuses :
|
||||
HttpSource(),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val id = EIGHTMUSES_SOURCE_ID
|
||||
|
||||
/**
|
||||
@ -74,10 +75,10 @@ class EightMuses : HttpSource(),
|
||||
private suspend fun obtainSiteMap() = siteMapCache.obtain {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
.await(Schedulers.io())
|
||||
.body!!.string()
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
.await(Schedulers.io())
|
||||
.body!!.string()
|
||||
|
||||
val parsed = Jsoup.parse(result)
|
||||
|
||||
@ -93,10 +94,10 @@ class EightMuses : HttpSource(),
|
||||
|
||||
override fun headersBuilder(): Headers.Builder {
|
||||
return Headers.Builder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
|
||||
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
|
||||
.add("Referer", "https://www.8muses.com")
|
||||
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
|
||||
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
|
||||
.add("Referer", "https://www.8muses.com")
|
||||
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
|
||||
}
|
||||
|
||||
private fun eightMusesGet(url: String): Request {
|
||||
@ -129,11 +130,11 @@ class EightMuses : HttpSource(),
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = if (!query.isBlank()) {
|
||||
"$baseUrl/search".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
} else {
|
||||
"$baseUrl/comics".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.newBuilder()
|
||||
}
|
||||
|
||||
urlBuilder.addQueryParameter("page", page.toString())
|
||||
@ -182,12 +183,14 @@ class EightMuses : HttpSource(),
|
||||
|
||||
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.flatMapSingle { response ->
|
||||
RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) {
|
||||
.asObservableSuccess()
|
||||
.flatMapSingle { response ->
|
||||
RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
parseResultsPage(response, dig)
|
||||
}.asSingle(GlobalScope.coroutineContext))
|
||||
}
|
||||
}.asSingle(GlobalScope.coroutineContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
|
||||
@ -197,32 +200,32 @@ class EightMuses : HttpSource(),
|
||||
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
|
||||
|
||||
return MangasPage(
|
||||
if (dig) {
|
||||
contents.albums.flatMap {
|
||||
val href = it.attr("href")
|
||||
val splitHref = href.split('/')
|
||||
obtainSiteMap().subMap(href).filter {
|
||||
it.key.split('/').size - splitHref.size == 1
|
||||
}.map { (key, _) ->
|
||||
SManga.create().apply {
|
||||
url = key
|
||||
|
||||
title = key.substringAfterLast('/').replace('-', ' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents.albums.map {
|
||||
if (dig) {
|
||||
contents.albums.flatMap {
|
||||
val href = it.attr("href")
|
||||
val splitHref = href.split('/')
|
||||
obtainSiteMap().subMap(href).filter {
|
||||
it.key.split('/').size - splitHref.size == 1
|
||||
}.map { (key, _) ->
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
url = key
|
||||
|
||||
title = it.select(".title-text").text()
|
||||
|
||||
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
|
||||
title = key.substringAfterLast('/').replace('-', ' ')
|
||||
}
|
||||
}
|
||||
},
|
||||
!onLastPage
|
||||
}
|
||||
} else {
|
||||
contents.albums.map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.select(".title-text").text()
|
||||
|
||||
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
|
||||
}
|
||||
}
|
||||
},
|
||||
!onLastPage
|
||||
)
|
||||
}
|
||||
|
||||
@ -243,10 +246,10 @@ class EightMuses : HttpSource(),
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -259,9 +262,11 @@ class EightMuses : HttpSource(),
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) {
|
||||
fetchAndParseChapterList("", manga.url)
|
||||
}.asSingle(GlobalScope.coroutineContext)).toObservable()
|
||||
return RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
fetchAndParseChapterList("", manga.url)
|
||||
}.asSingle(GlobalScope.coroutineContext)
|
||||
).toObservable()
|
||||
}
|
||||
|
||||
private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {
|
||||
@ -309,9 +314,9 @@ class EightMuses : HttpSource(),
|
||||
val contents = parseSelf(response.asJsoup())
|
||||
return contents.images.mapIndexed { index, element ->
|
||||
Page(
|
||||
index,
|
||||
element.attr("href"),
|
||||
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
|
||||
index,
|
||||
element.attr("href"),
|
||||
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -325,30 +330,30 @@ class EightMuses : HttpSource(),
|
||||
title = breadcrumbs.selectFirst("li:nth-last-child(1) > a").text()
|
||||
|
||||
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
|
||||
?.selectFirst(".lazyload")
|
||||
?.attr("data-src")?.let {
|
||||
baseUrl + it
|
||||
}
|
||||
?.selectFirst(".lazyload")
|
||||
?.attr("data-src")?.let {
|
||||
baseUrl + it
|
||||
}
|
||||
|
||||
tags.clear()
|
||||
tags += RaisedTag(
|
||||
EightMusesSearchMetadata.ARTIST_NAMESPACE,
|
||||
breadcrumbs.selectFirst("li:nth-child(2) > a").text(),
|
||||
EightMusesSearchMetadata.TAG_TYPE_DEFAULT
|
||||
EightMusesSearchMetadata.ARTIST_NAMESPACE,
|
||||
breadcrumbs.selectFirst("li:nth-child(2) > a").text(),
|
||||
EightMusesSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
tags += input.select(".album-tags a").map {
|
||||
RaisedTag(
|
||||
EightMusesSearchMetadata.TAGS_NAMESPACE,
|
||||
it.text(),
|
||||
EightMusesSearchMetadata.TAG_TYPE_DEFAULT
|
||||
EightMusesSearchMetadata.TAGS_NAMESPACE,
|
||||
it.text(),
|
||||
EightMusesSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : Filter.Select<String>(
|
||||
"Sort",
|
||||
SORT_OPTIONS.map { it.second }.toTypedArray()
|
||||
"Sort",
|
||||
SORT_OPTIONS.map { it.second }.toTypedArray()
|
||||
) {
|
||||
fun addToUri(url: HttpUrl.Builder) {
|
||||
url.addQueryParameter("sort", SORT_OPTIONS[state].first)
|
||||
@ -357,16 +362,16 @@ class EightMuses : HttpSource(),
|
||||
companion object {
|
||||
// <Internal, Display>
|
||||
private val SORT_OPTIONS = listOf(
|
||||
"" to "Views",
|
||||
"like" to "Likes",
|
||||
"date" to "Date",
|
||||
"az" to "A-Z"
|
||||
"" to "Views",
|
||||
"like" to "Likes",
|
||||
"date" to "Date",
|
||||
"az" to "A-Z"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter()
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
/**
|
||||
@ -379,8 +384,8 @@ class EightMuses : HttpSource(),
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"www.8muses.com",
|
||||
"8muses.com"
|
||||
"www.8muses.com",
|
||||
"8muses.com"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -19,8 +19,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document>, UrlImportableSource {
|
||||
class HentaiCafe(delegate: HttpSource) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
@ -32,18 +34,22 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
|
||||
initialized = true
|
||||
}))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,8 +63,8 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
thumbnailUrl = contentElement.child(0).child(0).attr("src")
|
||||
|
||||
fun filterableTagsOfType(type: String) = contentElement.select("a")
|
||||
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
|
||||
.map { it.text() }
|
||||
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
|
||||
.map { it.text() }
|
||||
|
||||
tags.clear()
|
||||
tags += filterableTagsOfType("tag").map {
|
||||
@ -78,29 +84,30 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
|
||||
override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) {
|
||||
client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { it.asJsoup() }
|
||||
.toSingle()
|
||||
.asObservableSuccess()
|
||||
.map { it.asJsoup() }
|
||||
.toSingle()
|
||||
}.map {
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain("/manga/read/${it.readerId}/en/0/1/")
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain("/manga/read/${it.readerId}/en/0/1/")
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
)
|
||||
}.toObservable()
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"hentai.cafe"
|
||||
"hentai.cafe"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
|
||||
return if (lcFirstPathSegment == "manga")
|
||||
return if (lcFirstPathSegment == "manga") {
|
||||
"https://hentai.cafe/${uri.pathSegments[2]}"
|
||||
else
|
||||
} else {
|
||||
"https://hentai.cafe/$lcFirstPathSegment"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,10 @@ import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
LewdSource<PururinSearchMetadata, Document>, UrlImportableSource {
|
||||
class Pururin(delegate: HttpSource) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PururinSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
@ -43,11 +45,11 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup())
|
||||
.andThen(Observable.just(manga))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup())
|
||||
.andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) {
|
||||
@ -87,9 +89,9 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
value.select("a").forEach { link ->
|
||||
val searchUrl = Uri.parse(link.attr("href"))
|
||||
tags += RaisedTag(
|
||||
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
|
||||
searchUrl.lastPathSegment!!.substringBefore("."),
|
||||
PururinSearchMetadata.TAG_TYPE_DEFAULT
|
||||
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
|
||||
searchUrl.lastPathSegment!!.substringBefore("."),
|
||||
PururinSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -99,8 +101,8 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"pururin.io",
|
||||
"www.pururin.io"
|
||||
"pururin.io",
|
||||
"www.pururin.io"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
|
@ -19,30 +19,33 @@ import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
|
||||
class Tsumino(delegate: HttpSource) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = TsuminoSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry")
|
||||
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
|
||||
return null
|
||||
}
|
||||
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
|
||||
@ -106,16 +109,18 @@ class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
character = newCharacter
|
||||
|
||||
input.getElementById("Tag")?.children()?.let {
|
||||
tags.addAll(it.map {
|
||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
})
|
||||
tags.addAll(
|
||||
it.map {
|
||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"www.tsumino.com",
|
||||
"tsumino.com"
|
||||
"www.tsumino.com",
|
||||
"tsumino.com"
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
@ -131,10 +131,14 @@ class SourceController :
|
||||
// Open the catalogue view.
|
||||
openCatalogue(source, BrowseSourceController(source))
|
||||
}
|
||||
Mode.SMART_SEARCH -> router.pushController(SmartSearchController(Bundle().apply {
|
||||
putLong(SmartSearchController.ARG_SOURCE_ID, source.id)
|
||||
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig)
|
||||
}).withFadeTransaction())
|
||||
Mode.SMART_SEARCH -> router.pushController(
|
||||
SmartSearchController(
|
||||
Bundle().apply {
|
||||
putLong(SmartSearchController.ARG_SOURCE_ID, source.id)
|
||||
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig)
|
||||
}
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -413,9 +413,9 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
val newSerialized = searches.map {
|
||||
"${source.id}:" + jsonObject(
|
||||
"name" to it.name,
|
||||
"query" to it.query,
|
||||
"filters" to filterSerializer.serialize(it.filterList)
|
||||
"name" to it.name,
|
||||
"query" to it.query,
|
||||
"filters" to filterSerializer.serialize(it.filterList)
|
||||
).toString()
|
||||
}
|
||||
prefs.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||
@ -430,9 +430,11 @@ open class BrowseSourcePresenter(
|
||||
val content = JsonParser.parseString(it.substringAfter(':')).obj
|
||||
val originalFilters = source.getFilterList()
|
||||
filterSerializer.deserialize(originalFilters, content["filters"].array)
|
||||
EXHSavedSearch(content["name"].string,
|
||||
content["query"].string,
|
||||
originalFilters)
|
||||
EXHSavedSearch(
|
||||
content["name"].string,
|
||||
content["query"].string,
|
||||
originalFilters
|
||||
)
|
||||
} catch (t: RuntimeException) {
|
||||
// Load failed
|
||||
Timber.e(t, "Failed to load saved search!")
|
||||
|
@ -393,7 +393,6 @@ class LibraryPresenter(
|
||||
manga: Manga,
|
||||
replace: Boolean
|
||||
) {
|
||||
|
||||
val flags = preferences.migrateFlags().get()
|
||||
val migrateChapters = MigrationFlags.hasChapters(flags)
|
||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
||||
|
@ -181,29 +181,34 @@ class MangaInfoPresenter(
|
||||
|
||||
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
|
||||
val originalManga = db.getManga(originalMangaId).await()
|
||||
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
|
||||
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
|
||||
val toInsert = if (originalManga.source == MERGED_SOURCE_ID) {
|
||||
originalManga.apply {
|
||||
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children
|
||||
if (originalChildren.any { it.source == manga.source && it.url == manga.url })
|
||||
if (originalChildren.any { it.source == manga.source && it.url == manga.url }) {
|
||||
throw IllegalArgumentException("This manga is already merged with the current manga!")
|
||||
}
|
||||
|
||||
url = MergedSource.MangaConfig(originalChildren + MergedSource.MangaSource(
|
||||
url = MergedSource.MangaConfig(
|
||||
originalChildren + MergedSource.MangaSource(
|
||||
manga.source,
|
||||
manga.url
|
||||
)).writeAsUrl(gson)
|
||||
)
|
||||
).writeAsUrl(gson)
|
||||
}
|
||||
} else {
|
||||
val newMangaConfig = MergedSource.MangaConfig(listOf(
|
||||
val newMangaConfig = MergedSource.MangaConfig(
|
||||
listOf(
|
||||
MergedSource.MangaSource(
|
||||
originalManga.source,
|
||||
originalManga.url
|
||||
originalManga.source,
|
||||
originalManga.url
|
||||
),
|
||||
MergedSource.MangaSource(
|
||||
manga.source,
|
||||
manga.url
|
||||
manga.source,
|
||||
manga.url
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
|
||||
copyFrom(originalManga)
|
||||
favorite = true
|
||||
|
@ -23,17 +23,22 @@ class MigrationMangaDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga
|
||||
val confirmString = applicationContext?.resources?.getQuantityString(confirmRes, mangaSet,
|
||||
mangaSet, (
|
||||
val confirmString = applicationContext?.resources?.getQuantityString(
|
||||
confirmRes, mangaSet,
|
||||
mangaSet,
|
||||
(
|
||||
if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped)
|
||||
else "")) ?: ""
|
||||
else ""
|
||||
)
|
||||
) ?: ""
|
||||
return MaterialDialog(activity!!)
|
||||
.message(text = confirmString)
|
||||
.positiveButton(if (copy) R.string.copy else R.string.migrate) {
|
||||
if (copy)
|
||||
if (copy) {
|
||||
(targetController as? MigrationListController)?.copyMangas()
|
||||
else
|
||||
} else {
|
||||
(targetController as? MigrationListController)?.migrateMangas()
|
||||
}
|
||||
}
|
||||
.negativeButton(android.R.string.no)
|
||||
}
|
||||
|
@ -31,10 +31,12 @@ class MigrationBottomSheetDialog(
|
||||
activity: Activity,
|
||||
theme: Int,
|
||||
private val listener:
|
||||
StartMigrationListener
|
||||
StartMigrationListener
|
||||
) :
|
||||
BottomSheetDialog(activity,
|
||||
theme) {
|
||||
BottomSheetDialog(
|
||||
activity,
|
||||
theme
|
||||
) {
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
@ -47,8 +49,9 @@ class MigrationBottomSheetDialog(
|
||||
// scroll.addView(view)
|
||||
|
||||
setContentView(view)
|
||||
if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
sourceGroup.orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
window?.setBackgroundDrawable(null)
|
||||
}
|
||||
|
||||
@ -63,8 +66,10 @@ class MigrationBottomSheetDialog(
|
||||
fab.setOnClickListener {
|
||||
preferences.skipPreMigration().set(skip_step.isChecked)
|
||||
listener.startMigration(
|
||||
if (use_smart_search.isChecked && extra_search_param_text.text.isNotBlank())
|
||||
extra_search_param_text.text.toString() else null)
|
||||
if (use_smart_search.isChecked && extra_search_param_text.text.isNotBlank()) {
|
||||
extra_search_param_text.text.toString()
|
||||
} else null
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@ -96,9 +101,12 @@ class MigrationBottomSheetDialog(
|
||||
|
||||
skip_step.isChecked = preferences.skipPreMigration().get()
|
||||
skip_step.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked)
|
||||
(listener as? Controller)?.activity?.toast(R.string.pre_migration_skip_toast,
|
||||
Toast.LENGTH_LONG)
|
||||
if (isChecked) {
|
||||
(listener as? Controller)?.activity?.toast(
|
||||
R.string.pre_migration_skip_toast,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,16 +9,21 @@ class MigrationSourceAdapter(
|
||||
var items: List<MigrationSourceItem>,
|
||||
val controllerPre: PreMigrationController
|
||||
) : FlexibleAdapter<MigrationSourceItem>(
|
||||
items,
|
||||
controllerPre,
|
||||
true
|
||||
items,
|
||||
controllerPre,
|
||||
true
|
||||
) {
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putParcelableArrayList(SELECTED_SOURCES_KEY, ArrayList(currentItems.map {
|
||||
it.asParcelable()
|
||||
}))
|
||||
outState.putParcelableArrayList(
|
||||
SELECTED_SOURCES_KEY,
|
||||
ArrayList(
|
||||
currentItems.map {
|
||||
it.asParcelable()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
|
@ -66,8 +66,8 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean) :
|
||||
val source = sourceManager.get(si.sourceId) as? HttpSource ?: return null
|
||||
|
||||
return MigrationSourceItem(
|
||||
source,
|
||||
si.sourceEnabled
|
||||
source,
|
||||
si.sourceEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,11 @@ import exh.util.updateLayoutParams
|
||||
import exh.util.updatePaddingRelative
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrationControllerBinding>(bundle), FlexibleAdapter
|
||||
.OnItemClickListener, StartMigrationListener {
|
||||
class PreMigrationController(bundle: Bundle? = null) :
|
||||
BaseController<PreMigrationControllerBinding>(bundle),
|
||||
FlexibleAdapter
|
||||
.OnItemClickListener,
|
||||
StartMigrationListener {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
@ -69,8 +72,10 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
|
||||
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
|
||||
}
|
||||
// offset the recycler by the fab's inset + some inset on top
|
||||
v.updatePaddingRelative(bottom = padding.bottom + (binding.fab.marginBottom) +
|
||||
fabBaseMarginBottom + (binding.fab.height))
|
||||
v.updatePaddingRelative(
|
||||
bottom = padding.bottom + (binding.fab.marginBottom) +
|
||||
fabBaseMarginBottom + (binding.fab.height)
|
||||
)
|
||||
}
|
||||
|
||||
binding.fab.setOnClickListener {
|
||||
@ -101,7 +106,8 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
|
||||
config.toList(),
|
||||
extraSearchParams = extraParam
|
||||
)
|
||||
).withFadeTransaction().tag(MigrationListController.TAG))
|
||||
).withFadeTransaction().tag(MigrationListController.TAG)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@ -136,10 +142,13 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
|
||||
.filter { it.lang in languages }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
sources =
|
||||
sources.filter { isEnabled(it.id.toString()) }.sortedBy { sourcesSaved.indexOf(it.id
|
||||
.toString())
|
||||
} +
|
||||
sources.filterNot { isEnabled(it.id.toString()) }
|
||||
sources.filter { isEnabled(it.id.toString()) }.sortedBy {
|
||||
sourcesSaved.indexOf(
|
||||
it.id
|
||||
.toString()
|
||||
)
|
||||
} +
|
||||
sources.filterNot { isEnabled(it.id.toString()) }
|
||||
|
||||
return sources
|
||||
}
|
||||
@ -167,9 +176,11 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
|
||||
}
|
||||
|
||||
fun create(mangaIds: List<Long>): PreMigrationController {
|
||||
return PreMigrationController(Bundle().apply {
|
||||
putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray())
|
||||
})
|
||||
return PreMigrationController(
|
||||
Bundle().apply {
|
||||
putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,8 +56,10 @@ import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MigrationListController(bundle: Bundle? = null) : BaseController<MigrationListControllerBinding>(bundle),
|
||||
MigrationProcessAdapter.MigrationProcessInterface, CoroutineScope {
|
||||
class MigrationListController(bundle: Bundle? = null) :
|
||||
BaseController<MigrationListControllerBinding>(bundle),
|
||||
MigrationProcessAdapter.MigrationProcessInterface,
|
||||
CoroutineScope {
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@ -93,7 +95,6 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
|
||||
super.onViewCreated(view)
|
||||
view.applyWindowInsetsForController()
|
||||
setTitle()
|
||||
@ -217,7 +218,8 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
|
||||
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
|
||||
val chapters = try {
|
||||
source.fetchChapterList(localManga)
|
||||
.toSingle().await(Schedulers.io()) } catch (e: java.lang.Exception) {
|
||||
.toSingle().await(Schedulers.io())
|
||||
} catch (e: java.lang.Exception) {
|
||||
Timber.e(e)
|
||||
emptyList<SChapter>()
|
||||
} ?: emptyList()
|
||||
@ -313,7 +315,6 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(position: Int, item: MenuItem) {
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_search_manually -> {
|
||||
launchUI {
|
||||
@ -488,9 +489,11 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
|
||||
const val TAG = "migration_list"
|
||||
|
||||
fun create(config: MigrationProcedureConfig): MigrationListController {
|
||||
return MigrationListController(Bundle().apply {
|
||||
putParcelable(CONFIG_EXTRA, config)
|
||||
})
|
||||
return MigrationListController(
|
||||
Bundle().apply {
|
||||
putParcelable(CONFIG_EXTRA, config)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,8 +42,12 @@ class MigrationProcessAdapter(
|
||||
if (allMangasDone()) menuItemListener.enableButtons()
|
||||
}
|
||||
|
||||
fun allMangasDone() = (items.all { it.manga.migrationStatus != MigrationStatus
|
||||
.RUNNUNG } && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND })
|
||||
fun allMangasDone() = (
|
||||
items.all {
|
||||
it.manga.migrationStatus != MigrationStatus
|
||||
.RUNNUNG
|
||||
} && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND }
|
||||
)
|
||||
|
||||
fun mangasSkipped() = (items.count { it.manga.migrationStatus == MigrationStatus.MANGA_NOT_FOUND })
|
||||
|
||||
@ -59,7 +63,8 @@ class MigrationProcessAdapter(
|
||||
migrateMangaInternal(
|
||||
manga.manga() ?: return@forEach,
|
||||
toMangaObj,
|
||||
!copy)
|
||||
!copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,10 +48,18 @@ class MigrationProcessHolder(
|
||||
val manga = item.manga.manga()
|
||||
val source = item.manga.mangaSource()
|
||||
|
||||
migration_menu.setVectorCompat(R.drawable.ic_more_vert_24dp, view.context
|
||||
.getResourceColor(R.attr.colorOnPrimary))
|
||||
skip_manga.setVectorCompat(R.drawable.ic_close_24dp, view.context.getResourceColor(R
|
||||
.attr.colorOnPrimary))
|
||||
migration_menu.setVectorCompat(
|
||||
R.drawable.ic_more_vert_24dp,
|
||||
view.context
|
||||
.getResourceColor(R.attr.colorOnPrimary)
|
||||
)
|
||||
skip_manga.setVectorCompat(
|
||||
R.drawable.ic_close_24dp,
|
||||
view.context.getResourceColor(
|
||||
R
|
||||
.attr.colorOnPrimary
|
||||
)
|
||||
)
|
||||
migration_menu.invisible()
|
||||
skip_manga.visible()
|
||||
migration_manga_card_to.resetManga()
|
||||
@ -87,7 +95,8 @@ class MigrationProcessHolder(
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (item.manga.mangaId != this@MigrationProcessHolder.item?.manga?.mangaId ||
|
||||
item.manga.migrationStatus == MigrationStatus.RUNNUNG) {
|
||||
item.manga.migrationStatus == MigrationStatus.RUNNUNG
|
||||
) {
|
||||
return@withContext
|
||||
}
|
||||
if (searchResult != null && resultSource != null) {
|
||||
@ -152,11 +161,15 @@ class MigrationProcessHolder(
|
||||
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
|
||||
|
||||
if (latestChapter > 0f) {
|
||||
manga_last_chapter_label.text = context.getString(R.string.latest_,
|
||||
DecimalFormat("#.#").format(latestChapter))
|
||||
manga_last_chapter_label.text = context.getString(
|
||||
R.string.latest_,
|
||||
DecimalFormat("#.#").format(latestChapter)
|
||||
)
|
||||
} else {
|
||||
manga_last_chapter_label.text = context.getString(R.string.latest_,
|
||||
context.getString(R.string.unknown))
|
||||
manga_last_chapter_label.text = context.getString(
|
||||
R.string.latest_,
|
||||
context.getString(R.string.unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,13 +63,13 @@ class SettingsEhController : SettingsController() {
|
||||
private fun Preference<*>.reconfigure(): Boolean {
|
||||
// Listen for change commit
|
||||
asObservable()
|
||||
.skip(1) // Skip first as it is emitted immediately
|
||||
.take(1) // Only listen for first commit
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
// Only listen for first change commit
|
||||
WarnConfigureDialogController.uploadSettings(router)
|
||||
}
|
||||
.skip(1) // Skip first as it is emitted immediately
|
||||
.take(1) // Only listen for first commit
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
// Only listen for first change commit
|
||||
WarnConfigureDialogController.uploadSettings(router)
|
||||
}
|
||||
|
||||
// Always return true to save changes
|
||||
return true
|
||||
@ -85,12 +85,12 @@ class SettingsEhController : SettingsController() {
|
||||
isPersistent = false
|
||||
defaultValue = false
|
||||
preferences.enableExhentai()
|
||||
.asObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
isChecked = it
|
||||
}
|
||||
.asObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
isChecked = it
|
||||
}
|
||||
|
||||
onChange { newVal ->
|
||||
newVal as Boolean
|
||||
@ -98,9 +98,11 @@ class SettingsEhController : SettingsController() {
|
||||
preferences.enableExhentai().set(false)
|
||||
true
|
||||
} else {
|
||||
router.pushController(RouterTransaction.with(LoginController())
|
||||
router.pushController(
|
||||
RouterTransaction.with(LoginController())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler()))
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -148,20 +150,20 @@ class SettingsEhController : SettingsController() {
|
||||
summary = "The quality of the downloaded images"
|
||||
title = "Image quality"
|
||||
entries = arrayOf(
|
||||
"Auto",
|
||||
"2400x",
|
||||
"1600x",
|
||||
"1280x",
|
||||
"980x",
|
||||
"780x"
|
||||
"Auto",
|
||||
"2400x",
|
||||
"1600x",
|
||||
"1280x",
|
||||
"980x",
|
||||
"780x"
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"auto",
|
||||
"ovrs_2400",
|
||||
"ovrs_1600",
|
||||
"high",
|
||||
"med",
|
||||
"low"
|
||||
"auto",
|
||||
"ovrs_2400",
|
||||
"ovrs_1600",
|
||||
"high",
|
||||
"med",
|
||||
"low"
|
||||
)
|
||||
|
||||
onChange { preferences.imageQuality().reconfigure() }
|
||||
@ -202,21 +204,21 @@ class SettingsEhController : SettingsController() {
|
||||
onClick {
|
||||
activity?.let { activity ->
|
||||
MaterialDialog(activity)
|
||||
.title(R.string.eh_force_sync_reset_title)
|
||||
.message(R.string.eh_force_sync_reset_message)
|
||||
.positiveButton(android.R.string.yes) {
|
||||
LocalFavoritesStorage().apply {
|
||||
getRealm().use {
|
||||
it.trans {
|
||||
clearSnapshots(it)
|
||||
}
|
||||
.title(R.string.eh_force_sync_reset_title)
|
||||
.message(R.string.eh_force_sync_reset_message)
|
||||
.positiveButton(android.R.string.yes) {
|
||||
LocalFavoritesStorage().apply {
|
||||
getRealm().use {
|
||||
it.trans {
|
||||
clearSnapshots(it)
|
||||
}
|
||||
}
|
||||
activity.toast("Sync state reset", Toast.LENGTH_LONG)
|
||||
}
|
||||
.negativeButton(android.R.string.no)
|
||||
.cancelable(false)
|
||||
.show()
|
||||
activity.toast("Sync state reset", Toast.LENGTH_LONG)
|
||||
}
|
||||
.negativeButton(android.R.string.no)
|
||||
.cancelable(false)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,18 +313,18 @@ class SettingsEhController : SettingsController() {
|
||||
}
|
||||
|
||||
"""
|
||||
$statsText
|
||||
$statsText
|
||||
|
||||
Galleries that were checked in the last:
|
||||
- hour: ${metaInRelativeDuration(1.hours)}
|
||||
- 6 hours: ${metaInRelativeDuration(6.hours)}
|
||||
- 12 hours: ${metaInRelativeDuration(12.hours)}
|
||||
- day: ${metaInRelativeDuration(1.days)}
|
||||
- 2 days: ${metaInRelativeDuration(2.days)}
|
||||
- week: ${metaInRelativeDuration(7.days)}
|
||||
- month: ${metaInRelativeDuration(30.days)}
|
||||
- year: ${metaInRelativeDuration(365.days)}
|
||||
""".trimIndent()
|
||||
Galleries that were checked in the last:
|
||||
- hour: ${metaInRelativeDuration(1.hours)}
|
||||
- 6 hours: ${metaInRelativeDuration(6.hours)}
|
||||
- 12 hours: ${metaInRelativeDuration(12.hours)}
|
||||
- day: ${metaInRelativeDuration(1.days)}
|
||||
- 2 days: ${metaInRelativeDuration(2.days)}
|
||||
- week: ${metaInRelativeDuration(7.days)}
|
||||
- month: ${metaInRelativeDuration(30.days)}
|
||||
- year: ${metaInRelativeDuration(365.days)}
|
||||
""".trimIndent()
|
||||
} finally {
|
||||
progress.dismiss()
|
||||
}
|
||||
|
@ -75,10 +75,12 @@ class SettingsLibraryController : SettingsController() {
|
||||
intListPreference {
|
||||
key = Keys.eh_library_rounded_corners
|
||||
title = "Rounded Corner Radius"
|
||||
entriesRes = arrayOf(R.string.eh_rounded_corner_0, R.string.eh_rounded_corner_1,
|
||||
entriesRes = arrayOf(
|
||||
R.string.eh_rounded_corner_0, R.string.eh_rounded_corner_1,
|
||||
R.string.eh_rounded_corner_2, R.string.eh_rounded_corner_3, R.string.eh_rounded_corner_4,
|
||||
R.string.eh_rounded_corner_5, R.string.eh_rounded_corner_6, R.string.eh_rounded_corner_7,
|
||||
R.string.eh_rounded_corner_8, R.string.eh_rounded_corner_9, R.string.eh_rounded_corner_10)
|
||||
R.string.eh_rounded_corner_8, R.string.eh_rounded_corner_9, R.string.eh_rounded_corner_10
|
||||
)
|
||||
entryValues = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
|
||||
defaultValue = "4"
|
||||
summaryRes = R.string.eh_rounded_corners_desc
|
||||
@ -211,7 +213,8 @@ class SettingsLibraryController : SettingsController() {
|
||||
}
|
||||
}
|
||||
if (preferences.skipPreMigration().get() || preferences.migrationSources()
|
||||
.getOrDefault().isNotEmpty()) {
|
||||
.getOrDefault().isNotEmpty()
|
||||
) {
|
||||
switchPreference {
|
||||
key = Keys.skipPreMigration
|
||||
titleRes = R.string.pref_skip_pre_migration
|
||||
|
@ -26,19 +26,19 @@ const val HBROWSE_SOURCE_ID = LEWD_SOURCE_SERIES + 12
|
||||
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
|
||||
|
||||
private val DELEGATED_LEWD_SOURCES = listOf(
|
||||
HentaiCafe::class,
|
||||
Pururin::class,
|
||||
Tsumino::class
|
||||
HentaiCafe::class,
|
||||
Pururin::class,
|
||||
Tsumino::class
|
||||
)
|
||||
|
||||
val LIBRARY_UPDATE_EXCLUDED_SOURCES = listOf(
|
||||
EH_SOURCE_ID,
|
||||
EXH_SOURCE_ID,
|
||||
NHENTAI_SOURCE_ID,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
TSUMINO_SOURCE_ID,
|
||||
HITOMI_SOURCE_ID,
|
||||
PURURIN_SOURCE_ID
|
||||
EH_SOURCE_ID,
|
||||
EXH_SOURCE_ID,
|
||||
NHENTAI_SOURCE_ID,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
TSUMINO_SOURCE_ID,
|
||||
HITOMI_SOURCE_ID,
|
||||
PURURIN_SOURCE_ID
|
||||
)
|
||||
|
||||
private inline fun <reified T> delegatedSourceId(): Long {
|
||||
@ -54,6 +54,6 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
|
||||
|
||||
// This method MUST be fast!
|
||||
fun isLewdSource(source: Long) = source in 6900..6999 ||
|
||||
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
|
||||
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID
|
||||
|
@ -45,35 +45,41 @@ object EXHMigrations {
|
||||
if (oldVersion < 1) {
|
||||
db.inTransaction {
|
||||
// Migrate HentaiCafe source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6908
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6908
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
|
||||
// Migrate nhentai URLs
|
||||
val nhentaiManga = db.db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
|
||||
.build())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
|
||||
nhentaiManga.forEach {
|
||||
it.url = getUrlWithoutDomain(it.url)
|
||||
}
|
||||
|
||||
db.db.put()
|
||||
.objects(nhentaiManga)
|
||||
// Extremely slow without the resolver :/
|
||||
.withPutResolver(MangaUrlPutResolver())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
.objects(nhentaiManga)
|
||||
// Extremely slow without the resolver :/
|
||||
.withPutResolver(MangaUrlPutResolver())
|
||||
.prepare()
|
||||
.executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,14 +91,18 @@ object EXHMigrations {
|
||||
if (oldVersion < 8405) {
|
||||
db.inTransaction {
|
||||
// Migrate HBrowse source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
// Cancel old scheduler jobs with old ids
|
||||
@ -101,14 +111,18 @@ object EXHMigrations {
|
||||
if (oldVersion < 8408) {
|
||||
db.inTransaction {
|
||||
// Migrate Tsumino source IDs
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6909
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
|
||||
WHERE ${MangaTable.COL_SOURCE} = 6909
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 8409) {
|
||||
@ -214,10 +228,12 @@ object EXHMigrations {
|
||||
return try {
|
||||
val uri = URI(orig)
|
||||
var out = uri.path
|
||||
if (uri.query != null)
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
if (uri.fragment != null)
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
|
@ -37,15 +37,15 @@ class GalleryAdder {
|
||||
}
|
||||
} else {
|
||||
sourceManager.getVisibleCatalogueSources()
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.find {
|
||||
try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
false
|
||||
}
|
||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.find {
|
||||
try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
false
|
||||
}
|
||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||
}
|
||||
|
||||
// Map URL to manga URL
|
||||
@ -66,10 +66,10 @@ class GalleryAdder {
|
||||
|
||||
// Use manga in DB if possible, otherwise, make a new manga
|
||||
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
|
||||
?: Manga.create(source.id).apply {
|
||||
this.url = cleanedUrl
|
||||
title = realUrl
|
||||
}
|
||||
?: Manga.create(source.id).apply {
|
||||
this.url = cleanedUrl
|
||||
title = realUrl
|
||||
}
|
||||
|
||||
// Insert created manga if not in DB before fetching details
|
||||
// This allows us to keep the metadata when fetching details
|
||||
@ -111,8 +111,10 @@ class GalleryAdder {
|
||||
return GalleryAddEvent.Fail.NotFound(url)
|
||||
}
|
||||
|
||||
return GalleryAddEvent.Fail.Error(url,
|
||||
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim())
|
||||
return GalleryAddEvent.Fail.Error(
|
||||
url,
|
||||
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,6 +143,6 @@ sealed class GalleryAddEvent {
|
||||
) : Fail()
|
||||
|
||||
class NotFound(galleryUrl: String) :
|
||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ object DebugFunctions {
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
fun forceUpgradeMigration() {
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
EXHMigrations.upgrade(prefs)
|
||||
}
|
||||
|
||||
@ -38,8 +38,9 @@ object DebugFunctions {
|
||||
val metadataManga = db.getFavoriteMangaWithMetadata().await()
|
||||
|
||||
val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga ->
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID)
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
manga
|
||||
}.toList()
|
||||
|
||||
@ -56,13 +57,17 @@ object DebugFunctions {
|
||||
|
||||
fun addAllMangaInDatabaseToLibrary() {
|
||||
db.inTransaction {
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_FAVORITE} = 1
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_FAVORITE} = 1
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,25 +103,29 @@ object DebugFunctions {
|
||||
|
||||
fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j ->
|
||||
"""
|
||||
{
|
||||
info: ${j.id},
|
||||
isPeriod: ${j.isPeriodic},
|
||||
isPersisted: ${j.isPersisted},
|
||||
intervalMillis: ${j.intervalMillis},
|
||||
}
|
||||
{
|
||||
info: ${j.id},
|
||||
isPeriod: ${j.isPeriodic},
|
||||
isPersisted: ${j.isPersisted},
|
||||
intervalMillis: ${j.intervalMillis},
|
||||
}
|
||||
""".trimIndent()
|
||||
}.joinToString(",\n")
|
||||
|
||||
fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()
|
||||
|
||||
private fun convertSources(from: Long, to: Long) {
|
||||
db.lowLevel().executeSQL(RawQuery.builder()
|
||||
.query("""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $to
|
||||
WHERE ${MangaTable.COL_SOURCE} = $from
|
||||
""".trimIndent())
|
||||
db.lowLevel().executeSQL(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${MangaTable.TABLE}
|
||||
SET ${MangaTable.COL_SOURCE} = $to
|
||||
WHERE ${MangaTable.COL_SOURCE} = $from
|
||||
""".trimIndent()
|
||||
)
|
||||
.affectsTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -45,11 +45,11 @@ class SettingsDebugController : SettingsController() {
|
||||
val result = it.call(DebugFunctions)
|
||||
view.text = "Function returned result:\n\n$result"
|
||||
MaterialDialog(context)
|
||||
.customView(view = hView, scrollable = true)
|
||||
.customView(view = hView, scrollable = true)
|
||||
} catch (t: Throwable) {
|
||||
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
|
||||
MaterialDialog(context)
|
||||
.customView(view = hView, scrollable = true)
|
||||
.customView(view = hView, scrollable = true)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,13 @@ class EHentaiThrottleManager(
|
||||
// Throttle requests if necessary
|
||||
val now = System.currentTimeMillis()
|
||||
val timeDiff = now - lastThrottleTime
|
||||
if (timeDiff < throttleTime)
|
||||
if (timeDiff < throttleTime) {
|
||||
Thread.sleep(throttleTime - timeDiff)
|
||||
}
|
||||
|
||||
if (throttleTime < max)
|
||||
if (throttleTime < max) {
|
||||
throttleTime += inc
|
||||
}
|
||||
|
||||
lastThrottleTime = System.currentTimeMillis()
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
|
||||
|
||||
class EHentaiUpdateHelper(context: Context) {
|
||||
val parentLookupTable =
|
||||
MemAutoFlushingLookupTable(
|
||||
File(context.filesDir, "exh-plt.maftable"),
|
||||
GalleryEntry.Serializer()
|
||||
)
|
||||
MemAutoFlushingLookupTable(
|
||||
File(context.filesDir, "exh-plt.maftable"),
|
||||
GalleryEntry.Serializer()
|
||||
)
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
@ -30,22 +30,24 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
*/
|
||||
fun findAcceptedRootAndDiscardOthers(sourceId: Long, chapters: List<Chapter>): Single<Triple<ChapterChain, List<ChapterChain>, Boolean>> {
|
||||
// Find other chains
|
||||
val chainsObservable = Observable.merge(chapters.map { chapter ->
|
||||
db.getChapters(chapter.url).asRxSingle().toObservable()
|
||||
}).toList().map { allChapters ->
|
||||
val chainsObservable = Observable.merge(
|
||||
chapters.map { chapter ->
|
||||
db.getChapters(chapter.url).asRxSingle().toObservable()
|
||||
}
|
||||
).toList().map { allChapters ->
|
||||
allChapters.flatMap { innerChapters -> innerChapters.map { it.manga_id!! } }.distinct()
|
||||
}.flatMap { mangaIds ->
|
||||
Observable.merge(
|
||||
mangaIds.map { mangaId ->
|
||||
Single.zip(
|
||||
db.getManga(mangaId).asRxSingle(),
|
||||
db.getChaptersByMangaId(mangaId).asRxSingle()
|
||||
) { manga, chapters ->
|
||||
ChapterChain(manga, chapters)
|
||||
}.toObservable().filter {
|
||||
it.manga.source == sourceId
|
||||
}
|
||||
mangaIds.map { mangaId ->
|
||||
Single.zip(
|
||||
db.getManga(mangaId).asRxSingle(),
|
||||
db.getChaptersByMangaId(mangaId).asRxSingle()
|
||||
) { manga, chapters ->
|
||||
ChapterChain(manga, chapters)
|
||||
}.toObservable().filter {
|
||||
it.manga.source == sourceId
|
||||
}
|
||||
}
|
||||
)
|
||||
}.toList()
|
||||
|
||||
@ -66,65 +68,66 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
|
||||
// Copy chain chapters to curChapters
|
||||
val newChapters = toDiscard
|
||||
.flatMap { chain ->
|
||||
val meta by lazy {
|
||||
db.getFlatMetadataForManga(chain.manga.id!!)
|
||||
.executeAsBlocking()
|
||||
?.raise<EHentaiSearchMetadata>()
|
||||
}
|
||||
|
||||
chain.chapters.map { chapter ->
|
||||
// Convert old style chapters to new style chapters if possible
|
||||
if (chapter.date_upload <= 0 &&
|
||||
meta?.datePosted != null &&
|
||||
meta?.title != null) {
|
||||
chapter.name = meta!!.title!!
|
||||
chapter.date_upload = meta!!.datePosted!!
|
||||
}
|
||||
chapter
|
||||
}
|
||||
.flatMap { chain ->
|
||||
val meta by lazy {
|
||||
db.getFlatMetadataForManga(chain.manga.id!!)
|
||||
.executeAsBlocking()
|
||||
?.raise<EHentaiSearchMetadata>()
|
||||
}
|
||||
.fold(accepted.chapters) { curChapters, chapter ->
|
||||
val existing = curChapters.find { it.url == chapter.url }
|
||||
|
||||
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read
|
||||
|
||||
if (existing != null) {
|
||||
existing.read = existing.read || chapter.read
|
||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
existing.last_page_read = newLastPageRead
|
||||
}
|
||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||
curChapters
|
||||
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
|
||||
new = true
|
||||
curChapters + ChapterImpl().apply {
|
||||
manga_id = accepted.manga.id
|
||||
url = chapter.url
|
||||
name = chapter.name
|
||||
read = chapter.read
|
||||
bookmark = chapter.bookmark
|
||||
|
||||
last_page_read = chapter.last_page_read
|
||||
if (newLastPageRead != null && last_page_read <= 0) {
|
||||
last_page_read = newLastPageRead
|
||||
}
|
||||
|
||||
date_fetch = chapter.date_fetch
|
||||
date_upload = chapter.date_upload
|
||||
}
|
||||
} else curChapters
|
||||
}
|
||||
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
|
||||
.sortedBy { it.date_upload }
|
||||
.apply {
|
||||
mapIndexed { index, chapter ->
|
||||
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
|
||||
chapter.chapter_number = index + 1f
|
||||
chapter.source_order = lastIndex - index
|
||||
chain.chapters.map { chapter ->
|
||||
// Convert old style chapters to new style chapters if possible
|
||||
if (chapter.date_upload <= 0 &&
|
||||
meta?.datePosted != null &&
|
||||
meta?.title != null
|
||||
) {
|
||||
chapter.name = meta!!.title!!
|
||||
chapter.date_upload = meta!!.datePosted!!
|
||||
}
|
||||
chapter
|
||||
}
|
||||
}
|
||||
.fold(accepted.chapters) { curChapters, chapter ->
|
||||
val existing = curChapters.find { it.url == chapter.url }
|
||||
|
||||
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read
|
||||
|
||||
if (existing != null) {
|
||||
existing.read = existing.read || chapter.read
|
||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
existing.last_page_read = newLastPageRead
|
||||
}
|
||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||
curChapters
|
||||
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
|
||||
new = true
|
||||
curChapters + ChapterImpl().apply {
|
||||
manga_id = accepted.manga.id
|
||||
url = chapter.url
|
||||
name = chapter.name
|
||||
read = chapter.read
|
||||
bookmark = chapter.bookmark
|
||||
|
||||
last_page_read = chapter.last_page_read
|
||||
if (newLastPageRead != null && last_page_read <= 0) {
|
||||
last_page_read = newLastPageRead
|
||||
}
|
||||
|
||||
date_fetch = chapter.date_fetch
|
||||
date_upload = chapter.date_upload
|
||||
}
|
||||
} else curChapters
|
||||
}
|
||||
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
|
||||
.sortedBy { it.date_upload }
|
||||
.apply {
|
||||
mapIndexed { index, chapter ->
|
||||
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
|
||||
chapter.chapter_number = index + 1f
|
||||
chapter.source_order = lastIndex - index
|
||||
}
|
||||
}
|
||||
|
||||
toDiscard.forEach { it.manga.favorite = false }
|
||||
accepted.manga.favorite = true
|
||||
@ -165,8 +168,8 @@ data class GalleryEntry(val gId: String, val gToken: String) {
|
||||
override fun read(string: String): GalleryEntry {
|
||||
val colonIndex = string.indexOf(':')
|
||||
return GalleryEntry(
|
||||
string.substring(0, colonIndex),
|
||||
string.substring(colonIndex + 1, string.length)
|
||||
string.substring(0, colonIndex),
|
||||
string.substring(colonIndex + 1, string.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -137,17 +137,19 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
logger.d("Filtering manga and raising metadata...")
|
||||
val curTime = System.currentTimeMillis()
|
||||
val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga ->
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID)
|
||||
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val meta = db.getFlatMetadataForManga(manga.id!!).asRxSingle().await()
|
||||
?: return@mapNotNull null
|
||||
?: return@mapNotNull null
|
||||
|
||||
val raisedMeta = meta.raise<EHentaiSearchMetadata>()
|
||||
|
||||
// Don't update galleries too frequently
|
||||
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled))
|
||||
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minBy {
|
||||
it.date_upload
|
||||
@ -172,13 +174,15 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
break
|
||||
}
|
||||
|
||||
logger.d("Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
|
||||
index,
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration,
|
||||
modifiedThisIteration.size)
|
||||
logger.d(
|
||||
"Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
|
||||
index,
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration,
|
||||
modifiedThisIteration.size
|
||||
)
|
||||
|
||||
if (manga.id in modifiedThisIteration) {
|
||||
// We already processed this manga!
|
||||
@ -194,32 +198,37 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
failuresThisIteration++
|
||||
|
||||
logger.e("> Network error while updating gallery!", e)
|
||||
logger.e("> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration)
|
||||
logger.e(
|
||||
"> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chapters.isEmpty()) {
|
||||
logger.e("No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration)
|
||||
logger.e(
|
||||
"No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
|
||||
manga.id,
|
||||
meta.gId,
|
||||
meta.gToken,
|
||||
failuresThisIteration
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Find accepted root and discard others
|
||||
val (acceptedRoot, discardedRoots, hasNew) =
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||
|
||||
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
||||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
||||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })
|
||||
) {
|
||||
updatedManga += acceptedRoot.manga
|
||||
}
|
||||
|
||||
@ -229,13 +238,13 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
}
|
||||
} finally {
|
||||
prefs.eh_autoUpdateStats().set(
|
||||
gson.toJson(
|
||||
EHentaiUpdaterStats(
|
||||
startTime,
|
||||
allMeta.size,
|
||||
updatedThisIteration
|
||||
)
|
||||
gson.toJson(
|
||||
EHentaiUpdaterStats(
|
||||
startTime,
|
||||
allMeta.size,
|
||||
updatedThisIteration
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (updatedManga.isNotEmpty()) {
|
||||
@ -247,7 +256,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
// New, current
|
||||
suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||
val source = sourceManager.get(manga.source) as? EHentai
|
||||
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
|
||||
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
|
||||
|
||||
try {
|
||||
val updatedManga = source.fetchMangaDetails(manga).toSingle().await(Schedulers.io())
|
||||
@ -288,8 +297,10 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
|
||||
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
|
||||
return JobInfo.Builder(
|
||||
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
||||
else JOB_ID_UPDATE_BACKGROUND, componentName())
|
||||
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
||||
else JOB_ID_UPDATE_BACKGROUND,
|
||||
componentName()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.periodicBackgroundJobInfo(
|
||||
@ -298,29 +309,32 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
requireUnmetered: Boolean
|
||||
): JobInfo {
|
||||
return baseBackgroundJobInfo(false)
|
||||
.setPeriodic(period)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(
|
||||
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
else JobInfo.NETWORK_TYPE_ANY)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setRequiresBatteryNotLow(true)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
setEstimatedNetworkBytes(15000L * UPDATES_PER_ITERATION,
|
||||
1000L * UPDATES_PER_ITERATION)
|
||||
}
|
||||
.setPeriodic(period)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(
|
||||
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
else JobInfo.NETWORK_TYPE_ANY
|
||||
)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setRequiresBatteryNotLow(true)
|
||||
}
|
||||
.setRequiresCharging(requireCharging)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
setEstimatedNetworkBytes(
|
||||
15000L * UPDATES_PER_ITERATION,
|
||||
1000L * UPDATES_PER_ITERATION
|
||||
)
|
||||
}
|
||||
}
|
||||
.setRequiresCharging(requireCharging)
|
||||
// .setRequiresDeviceIdle(true) Job never seems to run with this
|
||||
.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Context.testBackgroundJobInfo(): JobInfo {
|
||||
return baseBackgroundJobInfo(true)
|
||||
.setOverrideDeadline(1)
|
||||
.build()
|
||||
.setOverrideDeadline(1)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun launchBackgroundTest(context: Context) {
|
||||
@ -343,9 +357,9 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
val wifiRestriction = "wifi" in restrictions
|
||||
|
||||
val jobInfo = context.periodicBackgroundJobInfo(
|
||||
interval.hours.inMilliseconds.longValue,
|
||||
acRestriction,
|
||||
wifiRestriction
|
||||
interval.hours.inMilliseconds.longValue,
|
||||
acRestriction,
|
||||
wifiRestriction
|
||||
)
|
||||
|
||||
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
|
||||
|
@ -10,15 +10,16 @@ class FavoritesIntroDialog {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
fun show(context: Context) = MaterialDialog(context)
|
||||
.title(text = "IMPORTANT FAVORITES SYNC NOTES")
|
||||
.message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSyncIntro().set(false)
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
.title(text = "IMPORTANT FAVORITES SYNC NOTES")
|
||||
.message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSyncIntro().set(false)
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
|
||||
private val FAVORITES_INTRO_TEXT = """
|
||||
private val FAVORITES_INTRO_TEXT =
|
||||
"""
|
||||
1. Changes to category names in the app are <b>NOT</b> synced! Please <i>change the category names on ExHentai instead</i>. The category names will be copied from the ExHentai servers every sync.
|
||||
<br><br>
|
||||
2. The favorite categories on ExHentai correspond to the <b>first 10 categories in the app</b> (excluding the 'Default' category). <i>Galleries in other categories will <b>NOT</b> be synced!</i>
|
||||
@ -30,5 +31,5 @@ class FavoritesIntroDialog {
|
||||
5. <b>Do NOT put favorites in multiple categories</b> (the app supports this). This can confuse the sync algorithm as ExHentai only allows each favorite to be in one category.
|
||||
<br><br>
|
||||
This dialog will only popup once. You can read these notes again by going to 'Settings > E-Hentai > Show favorites sync notes'.
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
private val exh by lazy {
|
||||
Injekt.get<SourceManager>().get(EXH_SOURCE_ID) as? EHentai
|
||||
?: EHentai(0, true, context)
|
||||
?: EHentai(0, true, context)
|
||||
}
|
||||
|
||||
private val storage = LocalFavoritesStorage()
|
||||
@ -82,8 +82,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
if (it.id in seenManga) {
|
||||
val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
|
||||
status.onNext(FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(it, inCategories))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(it, inCategories)
|
||||
)
|
||||
logger.w("Manga %s is in multiple categories!", it.id)
|
||||
return
|
||||
} else {
|
||||
@ -107,13 +109,17 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Take wake + wifi locks
|
||||
ignore { wakeLock?.release() }
|
||||
wakeLock = ignore {
|
||||
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"teh:ExhFavoritesSyncWakelock")
|
||||
context.powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"teh:ExhFavoritesSyncWakelock"
|
||||
)
|
||||
}
|
||||
ignore { wifiLock?.release() }
|
||||
wifiLock = ignore {
|
||||
context.wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL,
|
||||
"teh:ExhFavoritesSyncWifi")
|
||||
context.wifiManager.createWifiLock(
|
||||
WifiManager.WIFI_MODE_FULL,
|
||||
"teh:ExhFavoritesSyncWifi"
|
||||
)
|
||||
}
|
||||
|
||||
// Do not update galleries while syncing favorites
|
||||
@ -137,8 +143,9 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
// Apply change sets
|
||||
applyChangeSetToLocal(errorList, remoteChanges)
|
||||
if (localChanges != null)
|
||||
if (localChanges != null) {
|
||||
applyChangeSetToRemote(errorList, localChanges)
|
||||
}
|
||||
|
||||
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
|
||||
storage.snapshotEntries(realm)
|
||||
@ -173,10 +180,11 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
EHentaiUpdateWorker.scheduleBackground(context)
|
||||
}
|
||||
|
||||
if (errorList.isEmpty())
|
||||
if (errorList.isEmpty()) {
|
||||
status.onNext(FavoritesSyncStatus.Idle())
|
||||
else
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyRemoteCategories(errorList: MutableList<String>, categories: List<String>) {
|
||||
@ -217,22 +225,25 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
// Only insert categories if changed
|
||||
if (changed)
|
||||
if (changed) {
|
||||
db.insertCategories(newLocalCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) {
|
||||
val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(FormBody.Builder()
|
||||
.add("favcat", gallery.category.toString())
|
||||
.add("favnote", "")
|
||||
.add("apply", "Add to Favorites")
|
||||
.add("update", "1")
|
||||
.build())
|
||||
.build()
|
||||
.url(url)
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("favcat", gallery.category.toString())
|
||||
.add("favnote", "")
|
||||
.add("apply", "Add to Favorites")
|
||||
.add("update", "1")
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
||||
@ -271,8 +282,8 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
|
||||
|
||||
val formBody = FormBody.Builder()
|
||||
.add("ddact", "delete")
|
||||
.add("apply", "Apply")
|
||||
.add("ddact", "delete")
|
||||
.add("apply", "Apply")
|
||||
|
||||
// Add change set to form
|
||||
changeSet.removed.forEach {
|
||||
@ -280,9 +291,9 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://exhentai.org/favorites.php")
|
||||
.post(formBody.build())
|
||||
.build()
|
||||
.url("https://exhentai.org/favorites.php")
|
||||
.post(formBody.build())
|
||||
.build()
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to delete galleries from the remote servers!"
|
||||
@ -299,8 +310,12 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
||||
needWarnThrottle()))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.Processing(
|
||||
"Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
||||
needWarnThrottle()
|
||||
)
|
||||
)
|
||||
|
||||
throttleManager.throttle()
|
||||
|
||||
@ -317,8 +332,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
val url = it.getUrl()
|
||||
|
||||
// Consider both EX and EH sources
|
||||
listOf(db.getManga(url, EXH_SOURCE_ID),
|
||||
db.getManga(url, EH_SOURCE_ID)).forEach {
|
||||
listOf(
|
||||
db.getManga(url, EXH_SOURCE_ID),
|
||||
db.getManga(url, EH_SOURCE_ID)
|
||||
).forEach {
|
||||
val manga = it.executeAsBlocking()
|
||||
|
||||
if (manga?.favorite == true) {
|
||||
@ -340,16 +357,22 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
||||
needWarnThrottle()))
|
||||
status.onNext(
|
||||
FavoritesSyncStatus.Processing(
|
||||
"Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
||||
needWarnThrottle()
|
||||
)
|
||||
)
|
||||
|
||||
throttleManager.throttle()
|
||||
|
||||
// Import using gallery adder
|
||||
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
|
||||
true,
|
||||
exh,
|
||||
throttleManager::throttle)
|
||||
val result = galleryAdder.addGallery(
|
||||
"${exh.baseUrl}${it.getUrl()}",
|
||||
true,
|
||||
exh,
|
||||
throttleManager::throttle
|
||||
)
|
||||
|
||||
if (result is GalleryAddEvent.Fail) {
|
||||
if (result is GalleryAddEvent.Fail.NotFound) {
|
||||
@ -370,8 +393,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
throw IgnoredException()
|
||||
}
|
||||
} else if (result is GalleryAddEvent.Success) {
|
||||
insertedMangaCategories += MangaCategory.create(result.manga,
|
||||
categories[it.category]) to result.manga
|
||||
insertedMangaCategories += MangaCategory.create(
|
||||
result.manga,
|
||||
categories[it.category]
|
||||
) to result.manga
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,12 +404,12 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
insertedMangaCategories.chunked(10).map {
|
||||
Pair(it.map { it.first }, it.map { it.second })
|
||||
}.forEach {
|
||||
db.setMangaCategories(it.first, it.second)
|
||||
}
|
||||
db.setMangaCategories(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
fun needWarnThrottle() =
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
|
||||
class IgnoredException : RuntimeException()
|
||||
|
||||
@ -401,12 +426,15 @@ sealed class FavoritesSyncStatus(val message: String) {
|
||||
val manga: Manga,
|
||||
val categories: List<Category>
|
||||
) :
|
||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||
}
|
||||
class Initializing : FavoritesSyncStatus("Initializing sync")
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if (isThrottle)
|
||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||
else
|
||||
message)
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(
|
||||
if (isThrottle) {
|
||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||
} else {
|
||||
message
|
||||
}
|
||||
)
|
||||
class CompleteWithErrors(messages: List<String>) : FavoritesSyncStatus(messages.joinToString("\n"))
|
||||
}
|
||||
|
@ -14,41 +14,46 @@ class LocalFavoritesStorage {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val realmConfig = RealmConfiguration.Builder()
|
||||
.name("fav-sync")
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
.name("fav-sync")
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
|
||||
fun getRealm() = Realm.getInstance(realmConfig)
|
||||
|
||||
fun getChangedDbEntries(realm: Realm) =
|
||||
getChangedEntries(realm,
|
||||
getChangedEntries(
|
||||
realm,
|
||||
parseToFavoriteEntries(
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
|
||||
getChangedEntries(realm,
|
||||
getChangedEntries(
|
||||
realm,
|
||||
parseToFavoriteEntries(
|
||||
entries.asSequence().map {
|
||||
Pair(it.fav, it.manga.apply {
|
||||
entries.asSequence().map {
|
||||
Pair(
|
||||
it.fav,
|
||||
it.manga.apply {
|
||||
favorite = true
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun snapshotEntries(realm: Realm) {
|
||||
val dbMangas = parseToFavoriteEntries(
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
.executeAsBlocking()
|
||||
.asSequence()
|
||||
)
|
||||
)
|
||||
|
||||
// Delete old snapshot
|
||||
@ -70,29 +75,29 @@ class LocalFavoritesStorage {
|
||||
}
|
||||
|
||||
val removed = realm.where(FavoriteEntry::class.java)
|
||||
.findAll()
|
||||
.filter {
|
||||
queryListForEntry(terminated, it) == null
|
||||
}.map {
|
||||
realm.copyFromRealm(it)
|
||||
}
|
||||
.findAll()
|
||||
.filter {
|
||||
queryListForEntry(terminated, it) == null
|
||||
}.map {
|
||||
realm.copyFromRealm(it)
|
||||
}
|
||||
|
||||
return ChangeSet(added, removed)
|
||||
}
|
||||
|
||||
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
|
||||
where(FavoriteEntry::class.java)
|
||||
where(FavoriteEntry::class.java)
|
||||
.equalTo(FavoriteEntry::gid.name, entry.gid)
|
||||
.equalTo(FavoriteEntry::token.name, entry.token)
|
||||
.equalTo(FavoriteEntry::category.name, entry.category)
|
||||
.findFirst()
|
||||
|
||||
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
|
||||
list.find {
|
||||
it.gid == entry.gid &&
|
||||
list.find {
|
||||
it.gid == entry.gid &&
|
||||
it.token == entry.token &&
|
||||
it.category == entry.category
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
@ -100,28 +105,34 @@ class LocalFavoritesStorage {
|
||||
return manga.filter(this::validateDbManga).mapNotNull {
|
||||
val category = db.getCategoriesForManga(it).executeAsBlocking()
|
||||
|
||||
Pair(dbCategories.indexOf(category.firstOrNull()
|
||||
?: return@mapNotNull null), it)
|
||||
Pair(
|
||||
dbCategories.indexOf(
|
||||
category.firstOrNull()
|
||||
?: return@mapNotNull null
|
||||
),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
|
||||
manga.filter {
|
||||
validateDbManga(it.second)
|
||||
}.mapNotNull {
|
||||
FavoriteEntry().apply {
|
||||
title = it.second.title
|
||||
gid = EHentaiSearchMetadata.galleryId(it.second.url)
|
||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||
category = it.first
|
||||
manga.filter {
|
||||
validateDbManga(it.second)
|
||||
}.mapNotNull {
|
||||
FavoriteEntry().apply {
|
||||
title = it.second.title
|
||||
gid = EHentaiSearchMetadata.galleryId(it.second.url)
|
||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||
category = it.first
|
||||
|
||||
if (this.category > MAX_CATEGORIES)
|
||||
return@mapNotNull null
|
||||
if (this.category > MAX_CATEGORIES) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDbManga(manga: Manga) =
|
||||
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
|
||||
companion object {
|
||||
const val MAX_CATEGORIES = 9
|
||||
|
@ -71,39 +71,43 @@ class HitomiNozomi(
|
||||
}
|
||||
|
||||
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
||||
if (data == null)
|
||||
if (data == null) {
|
||||
return Single.just(emptyList())
|
||||
}
|
||||
|
||||
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
||||
val (offset, length) = data
|
||||
if (length > 100000000 || length <= 0)
|
||||
if (length > 100000000 || length <= 0) {
|
||||
return Single.just(emptyList())
|
||||
}
|
||||
|
||||
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
||||
.asObservable()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
.asObservable()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { inbuf ->
|
||||
if (inbuf.isEmpty()) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { inbuf ->
|
||||
if (inbuf.isEmpty())
|
||||
return@map emptyList<Int>()
|
||||
|
||||
val view = ByteCursor(inbuf)
|
||||
val numberOfGalleryIds = view.nextInt()
|
||||
val view = ByteCursor(inbuf)
|
||||
val numberOfGalleryIds = view.nextInt()
|
||||
|
||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||
|
||||
if (numberOfGalleryIds > 10000000 ||
|
||||
numberOfGalleryIds <= 0 ||
|
||||
inbuf.size != expectedLength) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
if (numberOfGalleryIds > 10000000 ||
|
||||
numberOfGalleryIds <= 0 ||
|
||||
inbuf.size != expectedLength
|
||||
) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
|
||||
(1..numberOfGalleryIds).map {
|
||||
view.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
(1..numberOfGalleryIds).map {
|
||||
view.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
||||
@ -112,10 +116,11 @@ class HitomiNozomi(
|
||||
for (i in 0 until top) {
|
||||
val dv1i = dv1[i].toInt() and 0xFF
|
||||
val dv2i = dv2[i].toInt() and 0xFF
|
||||
if (dv1i < dv2i)
|
||||
if (dv1i < dv2i) {
|
||||
return -1
|
||||
else if (dv1i > dv2i)
|
||||
} else if (dv1i > dv2i) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@ -185,16 +190,16 @@ class HitomiNozomi(
|
||||
}
|
||||
|
||||
return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { nodedata ->
|
||||
if (nodedata.isNotEmpty()) {
|
||||
decodeNode(nodedata)
|
||||
} else null
|
||||
}.toSingle()
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
it.body?.bytes() ?: ByteArray(0)
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { nodedata ->
|
||||
if (nodedata.isNotEmpty()) {
|
||||
decodeNode(nodedata)
|
||||
} else null
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
|
||||
@ -203,17 +208,19 @@ class HitomiNozomi(
|
||||
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
|
||||
}
|
||||
|
||||
return client.newCall(Request.Builder()
|
||||
return client.newCall(
|
||||
Request.Builder()
|
||||
.url(nozomiAddress)
|
||||
.build())
|
||||
.asObservableSuccess()
|
||||
.map { resp ->
|
||||
val body = resp.body!!.bytes()
|
||||
val cursor = ByteCursor(body)
|
||||
(1..body.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
.build()
|
||||
)
|
||||
.asObservableSuccess()
|
||||
.map { resp ->
|
||||
val body = resp.body!!.bytes()
|
||||
val cursor = ByteCursor(body)
|
||||
(1..body.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
}
|
||||
|
||||
private fun hashTerm(query: String): HashedTerm {
|
||||
@ -233,15 +240,18 @@ class HitomiNozomi(
|
||||
private val HASH_CHARSET = Charsets.UTF_8
|
||||
|
||||
fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request {
|
||||
return GET(url, Headers.Builder()
|
||||
return GET(
|
||||
url,
|
||||
Headers.Builder()
|
||||
.add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}")
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
||||
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
||||
.asObservableSuccess()
|
||||
.map { it.body!!.string().toLong() }
|
||||
.asObservableSuccess()
|
||||
.map { it.body!!.string().toLong() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
override fun createView(root: ViewGroup, textColor: Int, textSize: Float, textAlpha: Float): View {
|
||||
val view = LinearLayout(root.context)
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
view.setPadding(4.dpToPx, 0, 4.dpToPx, 4.dpToPx)
|
||||
val textView = TextView(view.context)
|
||||
@ -42,15 +42,16 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
textView.alpha = textAlpha
|
||||
textView.text = HtmlCompat.fromHtml(buildInfo(), HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
textView.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
view.addView(textView)
|
||||
this.textView = textView
|
||||
return view
|
||||
}
|
||||
|
||||
fun buildInfo() = """
|
||||
fun buildInfo() =
|
||||
"""
|
||||
<font color='green'>===[ ${context.getString(R.string.app_name)} ]===</font><br>
|
||||
<b>Build type:</b> ${BuildConfig.BUILD_TYPE}<br>
|
||||
<b>Debug mode:</b> ${BuildConfig.DEBUG.asEnabledString()}<br>
|
||||
@ -58,7 +59,7 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
||||
<b>Commit SHA:</b> ${BuildConfig.COMMIT_SHA}<br>
|
||||
<b>Log level:</b> ${EHLogLevel.currentLogLevel.name.toLowerCase()}<br>
|
||||
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().get().asEnabledString()}
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ enum class EHLogLevel(val description: String) {
|
||||
|
||||
fun init(context: Context) {
|
||||
curLogLevel = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getInt(PreferenceKeys.eh_logLevel, 0)
|
||||
.getInt(PreferenceKeys.eh_logLevel, 0)
|
||||
}
|
||||
|
||||
fun shouldLog(requiredLogLevel: EHLogLevel): Boolean {
|
||||
|
@ -35,31 +35,32 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank()) {
|
||||
null
|
||||
else
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) {
|
||||
forEach { action(it.key, it.value) }
|
||||
}
|
||||
|
||||
val ONGOING_SUFFIX = arrayOf(
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}",
|
||||
"<ongoing>",
|
||||
"ongoing",
|
||||
"[incomplete]",
|
||||
"(incomplete)",
|
||||
"{incomplete}",
|
||||
"<incomplete>",
|
||||
"incomplete",
|
||||
"[wip]",
|
||||
"(wip)",
|
||||
"{wip}",
|
||||
"<wip>",
|
||||
"wip"
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}",
|
||||
"<ongoing>",
|
||||
"ongoing",
|
||||
"[incomplete]",
|
||||
"(incomplete)",
|
||||
"{incomplete}",
|
||||
"<incomplete>",
|
||||
"incomplete",
|
||||
"[wip]",
|
||||
"(wip)",
|
||||
"{wip}",
|
||||
"<wip>",
|
||||
"wip"
|
||||
)
|
||||
|
||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
|
||||
|
@ -50,10 +50,11 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
// No title bug?
|
||||
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
||||
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault()) {
|
||||
altTitle ?: title
|
||||
else
|
||||
} else {
|
||||
title
|
||||
}
|
||||
titleObj?.let { manga.title = it }
|
||||
|
||||
// Set artist (if we can find one)
|
||||
@ -102,8 +103,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -117,24 +118,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http"))
|
||||
Uri.parse(it).pathSegments
|
||||
else
|
||||
it.split('/')
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http")) {
|
||||
Uri.parse(it).pathSegments
|
||||
} else {
|
||||
it.split('/')
|
||||
}
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun galleryId(url: String) = splitGalleryUrl(url)[1]
|
||||
|
||||
fun galleryToken(url: String) =
|
||||
splitGalleryUrl(url)[2]
|
||||
splitGalleryUrl(url)[2]
|
||||
|
||||
fun normalizeUrl(url: String) =
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
|
||||
fun idAndTokenToUrl(id: String, token: String) =
|
||||
"/g/$id/$token/?nw=always"
|
||||
"/g/$id/$token/?nw=always"
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -34,8 +34,8 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -31,15 +31,15 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.status = SManga.UNKNOWN
|
||||
|
||||
val detailsDesc = "Title: $title\n" +
|
||||
"Artist: $artist\n"
|
||||
"Artist: $artist\n"
|
||||
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
manga.description = listOf(detailsDesc, tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -50,6 +50,6 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
const val BASE_URL = "https://hentai.cafe"
|
||||
|
||||
fun hcIdFromUrl(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }
|
||||
url.split("/").last { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
@ -62,11 +62,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
detailsDesc += "Language: ${it.capitalize()}\n"
|
||||
}
|
||||
|
||||
if (series.isNotEmpty())
|
||||
if (series.isNotEmpty()) {
|
||||
detailsDesc += "Series: ${series.joinToString()}\n"
|
||||
}
|
||||
|
||||
if (characters.isNotEmpty())
|
||||
if (characters.isNotEmpty()) {
|
||||
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
||||
}
|
||||
|
||||
uploadDate?.let {
|
||||
detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n"
|
||||
@ -80,8 +82,8 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -93,9 +95,9 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
const val BASE_URL = "https://hitomi.la"
|
||||
|
||||
fun hlIdFromUrl(url: String) =
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
|
||||
fun urlFromHlId(id: String) =
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
}
|
||||
}
|
||||
|
@ -44,9 +44,11 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
if (mediaId != null) {
|
||||
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
|
||||
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs)
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs) {
|
||||
"cover"
|
||||
else "thumb"}.$it"
|
||||
} else {
|
||||
"thumb"
|
||||
}}.$it"
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,8 +93,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -108,14 +110,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun nhUrlToId(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
|
||||
fun nhIdToPath(id: Long) = "/g/$id/"
|
||||
}
|
||||
|
@ -41,11 +41,12 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.title = it
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
if (altTitles.isNotEmpty())
|
||||
if (altTitles.isNotEmpty()) {
|
||||
titleDesc += "Alternate Titles: \n" + altTitles
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
}
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
}
|
||||
}
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
artist?.let {
|
||||
@ -76,8 +77,8 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -87,9 +88,9 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
url.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last()
|
||||
}
|
||||
@ -102,7 +103,7 @@ enum class PervEdenLang(val id: Long) {
|
||||
|
||||
companion object {
|
||||
fun source(id: Long) =
|
||||
values().find { it.id == id }
|
||||
values().find { it.id == id }
|
||||
?: throw IllegalArgumentException("Unknown source ID: $id!")
|
||||
}
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -65,8 +65,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
val tagsDesc = tagsToDescription()
|
||||
|
||||
manga.description = listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -77,7 +77,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
val BASE_URL = "https://www.tsumino.com"
|
||||
|
||||
fun tmIdFromUrl(url: String) =
|
||||
Uri.parse(url).lastPathSegment
|
||||
Uri.parse(url).lastPathSegment
|
||||
|
||||
fun mangaUrlFromId(id: String) = "/Book/Info/$id"
|
||||
|
||||
|
@ -18,9 +18,9 @@ data class FlatMetadata(
|
||||
|
||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
|
||||
RaisedSearchMetadata.raiseFlattenGson
|
||||
.fromJson(metadata.extra, clazz.java).apply {
|
||||
fillBaseFields(this@FlatMetadata)
|
||||
}
|
||||
.fromJson(metadata.extra, clazz.java).apply {
|
||||
fillBaseFields(this@FlatMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> {
|
||||
|
@ -36,29 +36,29 @@ abstract class RaisedSearchMetadata {
|
||||
abstract fun copyTo(manga: SManga)
|
||||
|
||||
fun tagsToGenreString() =
|
||||
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
|
||||
fun tagsToDescription() =
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
|
||||
groupedTags.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
groupedTags.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
}
|
||||
this += joinedTags
|
||||
this += "\n"
|
||||
}
|
||||
this += joinedTags
|
||||
this += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun List<RaisedTag>.ofNamespace(ns: String): List<RaisedTag> {
|
||||
return filter { it.namespace == ns }
|
||||
@ -76,23 +76,23 @@ abstract class RaisedSearchMetadata {
|
||||
indexedExtra,
|
||||
0
|
||||
),
|
||||
tags.map {
|
||||
SearchTag(
|
||||
null,
|
||||
mangaId,
|
||||
it.namespace,
|
||||
it.name,
|
||||
it.type
|
||||
)
|
||||
},
|
||||
titles.map {
|
||||
SearchTitle(
|
||||
null,
|
||||
mangaId,
|
||||
it.title,
|
||||
it.type
|
||||
)
|
||||
}
|
||||
tags.map {
|
||||
SearchTag(
|
||||
null,
|
||||
mangaId,
|
||||
it.namespace,
|
||||
it.name,
|
||||
it.type
|
||||
)
|
||||
},
|
||||
titles.map {
|
||||
SearchTitle(
|
||||
null,
|
||||
mangaId,
|
||||
it.title,
|
||||
it.type
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ abstract class RaisedSearchMetadata {
|
||||
* @return the property value.
|
||||
*/
|
||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
|
||||
thisRef.getTitleOfType(type)
|
||||
thisRef.getTitleOfType(type)
|
||||
|
||||
/**
|
||||
* Sets the value of the property for the given object.
|
||||
@ -135,7 +135,7 @@ abstract class RaisedSearchMetadata {
|
||||
* @param value the value to set.
|
||||
*/
|
||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,22 +18,22 @@ import exh.metadata.sql.tables.SearchTagTable.COL_TYPE
|
||||
import exh.metadata.sql.tables.SearchTagTable.TABLE
|
||||
|
||||
class SearchTagTypeMapping : SQLiteTypeMapping<SearchTag>(
|
||||
SearchTagPutResolver(),
|
||||
SearchTagGetResolver(),
|
||||
SearchTagDeleteResolver()
|
||||
SearchTagPutResolver(),
|
||||
SearchTagGetResolver(),
|
||||
SearchTagDeleteResolver()
|
||||
)
|
||||
|
||||
class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: SearchTag) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: SearchTag) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: SearchTag) = ContentValues(5).apply {
|
||||
put(COL_ID, obj.id)
|
||||
@ -47,19 +47,19 @@ class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
|
||||
class SearchTagGetResolver : DefaultGetResolver<SearchTag>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): SearchTag = SearchTag(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
)
|
||||
}
|
||||
|
||||
class SearchTagDeleteResolver : DefaultDeleteResolver<SearchTag>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: SearchTag) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -17,22 +17,22 @@ import exh.metadata.sql.tables.SearchTitleTable.COL_TYPE
|
||||
import exh.metadata.sql.tables.SearchTitleTable.TABLE
|
||||
|
||||
class SearchTitleTypeMapping : SQLiteTypeMapping<SearchTitle>(
|
||||
SearchTitlePutResolver(),
|
||||
SearchTitleGetResolver(),
|
||||
SearchTitleDeleteResolver()
|
||||
SearchTitlePutResolver(),
|
||||
SearchTitleGetResolver(),
|
||||
SearchTitleDeleteResolver()
|
||||
)
|
||||
|
||||
class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: SearchTitle) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: SearchTitle) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: SearchTitle) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
@ -45,18 +45,18 @@ class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
|
||||
class SearchTitleGetResolver : DefaultGetResolver<SearchTitle>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): SearchTitle = SearchTitle(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
|
||||
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
|
||||
)
|
||||
}
|
||||
|
||||
class SearchTitleDeleteResolver : DefaultDeleteResolver<SearchTitle>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: SearchTitle) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
data class SearchTag(
|
||||
// Tag identifier, unique
|
||||
// Tag identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this tag is attached to
|
||||
// Metadata this tag is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Tag namespace
|
||||
// Tag namespace
|
||||
val namespace: String?,
|
||||
|
||||
// Tag name
|
||||
// Tag name
|
||||
val name: String,
|
||||
|
||||
// Tag type
|
||||
// Tag type
|
||||
val type: Int
|
||||
)
|
||||
|
@ -1,15 +1,15 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
data class SearchTitle(
|
||||
// Title identifier, unique
|
||||
// Title identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this title is attached to
|
||||
// Metadata this title is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Title
|
||||
// Title
|
||||
val title: String,
|
||||
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
val type: Int
|
||||
)
|
||||
|
@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTagTable
|
||||
|
||||
interface SearchTagQueries : DbProvider {
|
||||
fun getSearchTagsForManga(mangaId: Long) = db.get()
|
||||
.listOfObjects(SearchTag::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(SearchTag::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteSearchTagsForManga(mangaId: Long) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.where("${SearchTagTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertSearchTag(searchTag: SearchTag) = db.put().`object`(searchTag).prepare()
|
||||
|
||||
@ -31,10 +35,12 @@ interface SearchTagQueries : DbProvider {
|
||||
|
||||
fun deleteSearchTag(searchTag: SearchTag) = db.delete().`object`(searchTag).prepare()
|
||||
|
||||
fun deleteAllSearchTags() = db.delete().byQuery(DeleteQuery.builder()
|
||||
fun deleteAllSearchTags() = db.delete().byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTagTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setSearchTagsForManga(mangaId: Long, tags: List<SearchTag>) {
|
||||
db.inTransaction {
|
||||
|
@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTitleTable
|
||||
|
||||
interface SearchTitleQueries : DbProvider {
|
||||
fun getSearchTitlesForManga(mangaId: Long) = db.get()
|
||||
.listOfObjects(SearchTitle::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(SearchTitle::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteSearchTitlesForManga(mangaId: Long) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertSearchTitle(searchTitle: SearchTitle) = db.put().`object`(searchTitle).prepare()
|
||||
|
||||
@ -31,10 +35,12 @@ interface SearchTitleQueries : DbProvider {
|
||||
|
||||
fun deleteSearchTitle(searchTitle: SearchTitle) = db.delete().`object`(searchTitle).prepare()
|
||||
|
||||
fun deleteAllSearchTitle() = db.delete().byQuery(DeleteQuery.builder()
|
||||
fun deleteAllSearchTitle() = db.delete().byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(SearchTitleTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setSearchTitlesForManga(mangaId: Long, titles: List<SearchTitle>) {
|
||||
db.inTransaction {
|
||||
|
@ -17,7 +17,8 @@ object SearchMetadataTable {
|
||||
|
||||
// Insane foreign, primary key to avoid touch manga table
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_UPLOADER TEXT,
|
||||
$COL_EXTRA TEXT NOT NULL,
|
||||
|
@ -16,7 +16,8 @@ object SearchTagTable {
|
||||
const val COL_TYPE = "type"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_NAMESPACE TEXT,
|
||||
|
@ -14,7 +14,8 @@ object SearchTitleTable {
|
||||
const val COL_TYPE = "type"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
|
@ -9,13 +9,14 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
private val HIDE_SCRIPT = """
|
||||
document.querySelector("#forgot_button").style.visibility = "hidden";
|
||||
document.querySelector("#signup_button").style.visibility = "hidden";
|
||||
document.querySelector("#announcement").style.visibility = "hidden";
|
||||
document.querySelector("nav").style.visibility = "hidden";
|
||||
document.querySelector("footer").style.visibility = "hidden";
|
||||
""".trimIndent()
|
||||
private val HIDE_SCRIPT =
|
||||
"""
|
||||
document.querySelector("#forgot_button").style.visibility = "hidden";
|
||||
document.querySelector("#signup_button").style.visibility = "hidden";
|
||||
document.querySelector("#announcement").style.visibility = "hidden";
|
||||
document.querySelector("nav").style.visibility = "hidden";
|
||||
document.querySelector("footer").style.visibility = "hidden";
|
||||
""".trimIndent()
|
||||
|
||||
private fun verifyComplete(url: String): Boolean {
|
||||
return url.toHttpUrlOrNull()?.let { parsed ->
|
||||
@ -28,14 +29,14 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
response.interceptAsHtml { doc ->
|
||||
if (doc.title().trim().equals("Login - MangaDex", true)) {
|
||||
BrowserActionActivity.launchAction(
|
||||
Injekt.get<Application>(),
|
||||
::verifyComplete,
|
||||
HIDE_SCRIPT,
|
||||
"https://mangadex.org/login",
|
||||
"Login",
|
||||
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
Injekt.get<Application>(),
|
||||
::verifyComplete,
|
||||
HIDE_SCRIPT,
|
||||
"https://mangadex.org/login",
|
||||
"Login",
|
||||
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -43,43 +44,43 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
}
|
||||
|
||||
val MANGADEX_SOURCE_IDS = listOf(
|
||||
2499283573021220255,
|
||||
8033579885162383068,
|
||||
1952071260038453057,
|
||||
2098905203823335614,
|
||||
5098537545549490547,
|
||||
4505830566611664829,
|
||||
9194073792736219759,
|
||||
6400665728063187402,
|
||||
4938773340256184018,
|
||||
5860541308324630662,
|
||||
5189216366882819742,
|
||||
2655149515337070132,
|
||||
1145824452519314725,
|
||||
3846770256925560569,
|
||||
3807502156582598786,
|
||||
4284949320785450865,
|
||||
5463447640980279236,
|
||||
8578871918181236609,
|
||||
6750440049024086587,
|
||||
3339599426223341161,
|
||||
5148895169070562838,
|
||||
1493666528525752601,
|
||||
1713554459881080228,
|
||||
4150470519566206911,
|
||||
1347402746269051958,
|
||||
3578612018159256808,
|
||||
425785191804166217,
|
||||
8254121249433835847,
|
||||
3260701926561129943,
|
||||
1411768577036936240,
|
||||
3285208643537017688,
|
||||
737986167355114438,
|
||||
1471784905273036181,
|
||||
5967745367608513818,
|
||||
3781216447842245147,
|
||||
4774459486579224459,
|
||||
4710920497926776490,
|
||||
5779037855201976894
|
||||
2499283573021220255,
|
||||
8033579885162383068,
|
||||
1952071260038453057,
|
||||
2098905203823335614,
|
||||
5098537545549490547,
|
||||
4505830566611664829,
|
||||
9194073792736219759,
|
||||
6400665728063187402,
|
||||
4938773340256184018,
|
||||
5860541308324630662,
|
||||
5189216366882819742,
|
||||
2655149515337070132,
|
||||
1145824452519314725,
|
||||
3846770256925560569,
|
||||
3807502156582598786,
|
||||
4284949320785450865,
|
||||
5463447640980279236,
|
||||
8578871918181236609,
|
||||
6750440049024086587,
|
||||
3339599426223341161,
|
||||
5148895169070562838,
|
||||
1493666528525752601,
|
||||
1713554459881080228,
|
||||
4150470519566206911,
|
||||
1347402746269051958,
|
||||
3578612018159256808,
|
||||
425785191804166217,
|
||||
8254121249433835847,
|
||||
3260701926561129943,
|
||||
1411768577036936240,
|
||||
3285208643537017688,
|
||||
737986167355114438,
|
||||
1471784905273036181,
|
||||
5967745367608513818,
|
||||
3781216447842245147,
|
||||
4774459486579224459,
|
||||
4710920497926776490,
|
||||
5779037855201976894
|
||||
)
|
||||
const val MANGADEX_DOMAIN = "mangadex.org"
|
||||
|
@ -16,8 +16,10 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
|
||||
}
|
||||
|
||||
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
||||
return ((EH_INTERCEPTORS[sourceId] ?: emptyList()) +
|
||||
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())).merge()
|
||||
return (
|
||||
(EH_INTERCEPTORS[sourceId] ?: emptyList()) +
|
||||
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())
|
||||
).merge()
|
||||
}
|
||||
|
||||
fun List<EHInterceptor>.merge(): EHInterceptor {
|
||||
@ -30,12 +32,12 @@ fun List<EHInterceptor>.merge(): EHInterceptor {
|
||||
|
||||
private const val EH_UNIVERSAL_INTERCEPTOR = -1L
|
||||
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
|
||||
EH_UNIVERSAL_INTERCEPTOR to listOf(
|
||||
CAPTCHA_DETECTION_PATCH // Auto captcha detection
|
||||
),
|
||||
EH_UNIVERSAL_INTERCEPTOR to listOf(
|
||||
CAPTCHA_DETECTION_PATCH // Auto captcha detection
|
||||
),
|
||||
|
||||
// MangaDex login support
|
||||
*MANGADEX_SOURCE_IDS.map { id ->
|
||||
id to listOf(MANGADEX_LOGIN_PATCH)
|
||||
}.toTypedArray()
|
||||
// MangaDex login support
|
||||
*MANGADEX_SOURCE_IDS.map { id ->
|
||||
id to listOf(MANGADEX_LOGIN_PATCH)
|
||||
}.toTypedArray()
|
||||
)
|
||||
|
@ -13,9 +13,9 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
||||
// Found it, allow the user to solve this thing
|
||||
BrowserActionActivity.launchUniversal(
|
||||
Injekt.get<Application>(),
|
||||
sourceId,
|
||||
request.url.toString()
|
||||
Injekt.get<Application>(),
|
||||
sourceId,
|
||||
request.url.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,11 @@ class SearchEngine {
|
||||
component: Text?
|
||||
): Pair<String, List<String>>? {
|
||||
val maybeLenientComponent = component?.let {
|
||||
if (!it.exact)
|
||||
it.asLenientTagQueries()
|
||||
else
|
||||
listOf(it.asQuery())
|
||||
if (!it.exact) {
|
||||
it.asLenientTagQueries()
|
||||
} else {
|
||||
listOf(it.asQuery())
|
||||
}
|
||||
}
|
||||
val componentTagQuery = maybeLenientComponent?.let {
|
||||
val params = mutableListOf<String>()
|
||||
@ -25,11 +26,12 @@ class SearchEngine {
|
||||
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
||||
}
|
||||
return if (namespace != null) {
|
||||
var query = """
|
||||
var query =
|
||||
"""
|
||||
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
||||
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
val params = mutableListOf(escapeLike(namespace))
|
||||
if (componentTagQuery != null) {
|
||||
query += "\n AND ${componentTagQuery.first}"
|
||||
@ -39,18 +41,20 @@ class SearchEngine {
|
||||
"$query)" to params
|
||||
} else if (component != null) {
|
||||
// Match title + tags
|
||||
val tagQuery = """
|
||||
val tagQuery =
|
||||
"""
|
||||
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||
WHERE ${componentTagQuery!!.first}
|
||||
""".trimIndent() to componentTagQuery.second
|
||||
""".trimIndent() to componentTagQuery.second
|
||||
|
||||
val titleQuery = """
|
||||
val titleQuery =
|
||||
"""
|
||||
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
|
||||
WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
|
||||
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
||||
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
||||
|
||||
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
|
||||
(tagQuery.second + titleQuery.second)
|
||||
(tagQuery.second + titleQuery.second)
|
||||
} else null
|
||||
}
|
||||
|
||||
@ -86,22 +90,25 @@ class SearchEngine {
|
||||
}
|
||||
|
||||
val completeParams = mutableListOf<String>()
|
||||
var baseQuery = """
|
||||
var baseQuery =
|
||||
"""
|
||||
SELECT ${SearchMetadataTable.COL_MANGA_ID}
|
||||
FROM ${SearchMetadataTable.TABLE} meta
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
include.forEachIndexed { index, pair ->
|
||||
baseQuery += "\n" + ("""
|
||||
baseQuery += "\n" + (
|
||||
"""
|
||||
INNER JOIN ${pair.first} i$index
|
||||
ON i$index.$COL_MANGA_ID = meta.${SearchMetadataTable.COL_MANGA_ID}
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
completeParams += pair.second
|
||||
}
|
||||
|
||||
exclude.forEach {
|
||||
wheres += """
|
||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||
""".trimIndent()
|
||||
whereParams += it.second
|
||||
}
|
||||
@ -196,8 +203,8 @@ class SearchEngine {
|
||||
|
||||
fun escapeLike(string: String): String {
|
||||
return string.replace("\\", "\\\\")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,13 +28,13 @@ class Text : QueryComponent() {
|
||||
fun asLenientTagQueries(): List<String> {
|
||||
if (lenientTagQueries == null) {
|
||||
lenientTagQueries = listOf(
|
||||
// Match beginning of tag
|
||||
rBaseBuilder().append("%").toString(),
|
||||
// Tag word matcher (that matches multiple words)
|
||||
// Can't make it match a single word in Realm :(
|
||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||
rBaseBuilder().append(" ").toString()
|
||||
// Match beginning of tag
|
||||
rBaseBuilder().append("%").toString(),
|
||||
// Tag word matcher (that matches multiple words)
|
||||
// Can't make it match a single word in Realm :(
|
||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||
rBaseBuilder().append(" ").toString()
|
||||
)
|
||||
}
|
||||
return lenientTagQueries!!
|
||||
@ -52,11 +52,11 @@ class Text : QueryComponent() {
|
||||
return builder
|
||||
}
|
||||
|
||||
fun rawTextOnly() = if (rawText != null)
|
||||
fun rawTextOnly() = if (rawText != null) {
|
||||
rawText!!
|
||||
else {
|
||||
} else {
|
||||
rawText = components
|
||||
.joinToString(separator = "", transform = { it.rawText })
|
||||
.joinToString(separator = "", transform = { it.rawText })
|
||||
rawText!!
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,9 @@ class SmartSearchEngine(
|
||||
} else title
|
||||
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
||||
|
||||
if (searchResults.mangas.size == 1)
|
||||
if (searchResults.mangas.size == 1) {
|
||||
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
|
||||
}
|
||||
|
||||
searchResults.mangas.map {
|
||||
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)
|
||||
|
@ -7,38 +7,38 @@ object BlacklistedSources {
|
||||
val PERVEDEN_EN_EXT_SOURCES = listOf(4673633799850248749)
|
||||
val PERVEDEN_IT_EXT_SOURCES = listOf(1433898225963724122)
|
||||
val EHENTAI_EXT_SOURCES = listOf(
|
||||
8100626124886895451,
|
||||
57122881048805941,
|
||||
4678440076103929247,
|
||||
1876021963378735852,
|
||||
3955189842350477641,
|
||||
4348288691341764259,
|
||||
773611868725221145,
|
||||
5759417018342755550,
|
||||
825187715438990384,
|
||||
6116711405602166104,
|
||||
7151438547982231541,
|
||||
2171445159732592630,
|
||||
3032959619549451093,
|
||||
5980349886941016589,
|
||||
6073266008352078708,
|
||||
5499077866612745456,
|
||||
6140480779421365791
|
||||
8100626124886895451,
|
||||
57122881048805941,
|
||||
4678440076103929247,
|
||||
1876021963378735852,
|
||||
3955189842350477641,
|
||||
4348288691341764259,
|
||||
773611868725221145,
|
||||
5759417018342755550,
|
||||
825187715438990384,
|
||||
6116711405602166104,
|
||||
7151438547982231541,
|
||||
2171445159732592630,
|
||||
3032959619549451093,
|
||||
5980349886941016589,
|
||||
6073266008352078708,
|
||||
5499077866612745456,
|
||||
6140480779421365791
|
||||
)
|
||||
|
||||
val BLACKLISTED_EXT_SOURCES = NHENTAI_EXT_SOURCES +
|
||||
PERVEDEN_EN_EXT_SOURCES +
|
||||
PERVEDEN_IT_EXT_SOURCES +
|
||||
EHENTAI_EXT_SOURCES
|
||||
PERVEDEN_EN_EXT_SOURCES +
|
||||
PERVEDEN_IT_EXT_SOURCES +
|
||||
EHENTAI_EXT_SOURCES
|
||||
|
||||
val BLACKLISTED_EXTENSIONS = listOf(
|
||||
"eu.kanade.tachiyomi.extension.all.ehentai",
|
||||
"eu.kanade.tachiyomi.extension.all.nhentai",
|
||||
"eu.kanade.tachiyomi.extension.en.perveden",
|
||||
"eu.kanade.tachiyomi.extension.it.perveden"
|
||||
"eu.kanade.tachiyomi.extension.all.ehentai",
|
||||
"eu.kanade.tachiyomi.extension.all.nhentai",
|
||||
"eu.kanade.tachiyomi.extension.en.perveden",
|
||||
"eu.kanade.tachiyomi.extension.it.perveden"
|
||||
)
|
||||
|
||||
val HIDDEN_SOURCES = listOf(
|
||||
MERGED_SOURCE_ID
|
||||
MERGED_SOURCE_ID
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -26,7 +26,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@ -36,7 +36,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -44,7 +44,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@ -52,7 +52,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -60,7 +60,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@ -68,7 +68,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@ -76,7 +76,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@ -84,7 +84,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@ -92,7 +92,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
@ -240,7 +240,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
|
||||
private fun ensureDelegateCompatible() {
|
||||
if (versionId != delegate.versionId ||
|
||||
lang != delegate.lang) {
|
||||
lang != delegate.lang
|
||||
) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class EnhancedHttpSource(
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -31,7 +31,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@ -41,7 +41,7 @@ class EnhancedHttpSource(
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -49,7 +49,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@ -57,7 +57,7 @@ class EnhancedHttpSource(
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
@ -65,7 +65,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@ -73,7 +73,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@ -81,7 +81,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@ -89,7 +89,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@ -97,7 +97,7 @@ class EnhancedHttpSource(
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
@ -153,7 +153,7 @@ class EnhancedHttpSource(
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
@ -209,7 +209,7 @@ class EnhancedHttpSource(
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
|
@ -14,7 +14,7 @@ class ConfiguringDialogController : DialogController() {
|
||||
private var materialDialog: MaterialDialog? = null
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
if (savedViewState == null)
|
||||
if (savedViewState == null) {
|
||||
thread {
|
||||
try {
|
||||
EHConfigurator().configureAll()
|
||||
@ -25,10 +25,10 @@ class ConfiguringDialogController : DialogController() {
|
||||
activity?.let {
|
||||
it.runOnUiThread {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Configuration failed!")
|
||||
.message(text = "An error occurred during the configuration process: " + e.message)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "Configuration failed!")
|
||||
.message(text = "An error occurred during the configuration process: " + e.message)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
Timber.e(e, "Configuration error!")
|
||||
@ -37,14 +37,15 @@ class ConfiguringDialogController : DialogController() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = "Uploading settings to server")
|
||||
.message(text = "Please wait, this may take some time...")
|
||||
.cancelable(false)
|
||||
.also {
|
||||
materialDialog = it
|
||||
}
|
||||
.title(text = "Uploading settings to server")
|
||||
.message(text = "Please wait, this may take some time...")
|
||||
.cancelable(false)
|
||||
.also {
|
||||
materialDialog = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
|
@ -18,11 +18,11 @@ class EHConfigurator {
|
||||
private val sources: SourceManager by injectLazy()
|
||||
|
||||
private val configuratorClient = OkHttpClient.Builder()
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
|
||||
.addHeader("Cookie", cookiesHeader(sp))
|
||||
.addHeader("Cookie", cookiesHeader(sp))
|
||||
|
||||
private fun EHentai.execProfileActions(
|
||||
action: String,
|
||||
@ -30,15 +30,19 @@ class EHConfigurator {
|
||||
set: String,
|
||||
sp: Int
|
||||
) =
|
||||
configuratorClient.newCall(requestWithCreds(sp)
|
||||
configuratorClient.newCall(
|
||||
requestWithCreds(sp)
|
||||
.url(uconfigUrl)
|
||||
.post(FormBody.Builder()
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("profile_action", action)
|
||||
.add("profile_name", name)
|
||||
.add("profile_set", set)
|
||||
.build())
|
||||
.build())
|
||||
.execute()
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.execute()
|
||||
|
||||
private val EHentai.uconfigUrl get() = baseUrl + UCONFIG_URL
|
||||
|
||||
@ -47,10 +51,12 @@ class EHConfigurator {
|
||||
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
|
||||
|
||||
// Get hath perks
|
||||
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds()
|
||||
val perksPage = configuratorClient.newCall(
|
||||
ehSource.requestWithCreds()
|
||||
.url(HATH_PERKS_URL)
|
||||
.build())
|
||||
.execute().asJsoup()
|
||||
.build()
|
||||
)
|
||||
.execute().asJsoup()
|
||||
|
||||
val hathPerks = EHHathPerksResponse()
|
||||
|
||||
@ -97,24 +103,29 @@ class EHConfigurator {
|
||||
}
|
||||
|
||||
// No profile slots left :(
|
||||
if (availableProfiles.isEmpty())
|
||||
if (availableProfiles.isEmpty()) {
|
||||
throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
|
||||
|
||||
}
|
||||
// Create profile in available slot
|
||||
|
||||
val slot = availableProfiles.first()
|
||||
val response = source.execProfileActions("create",
|
||||
PROFILE_NAME,
|
||||
slot.toString(),
|
||||
1)
|
||||
val response = source.execProfileActions(
|
||||
"create",
|
||||
PROFILE_NAME,
|
||||
slot.toString(),
|
||||
1
|
||||
)
|
||||
|
||||
// Build new profile
|
||||
val form = EhUConfigBuilder().build(hathPerks)
|
||||
|
||||
// Send new profile to server
|
||||
configuratorClient.newCall(source.requestWithCreds(sp = slot)
|
||||
configuratorClient.newCall(
|
||||
source.requestWithCreds(sp = slot)
|
||||
.url(source.uconfigUrl)
|
||||
.post(form)
|
||||
.build()).execute()
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
// Persist slot + sk
|
||||
source.spPref().set(slot)
|
||||
@ -129,12 +140,15 @@ class EHConfigurator {
|
||||
it.startsWith("hath_perks=")
|
||||
}?.removePrefix("hath_perks=")?.substringBefore(';')
|
||||
|
||||
if (keyCookie != null)
|
||||
if (keyCookie != null) {
|
||||
prefs.eh_settingsKey().set(keyCookie)
|
||||
if (sessionCookie != null)
|
||||
}
|
||||
if (sessionCookie != null) {
|
||||
prefs.eh_sessionCookie().set(sessionCookie)
|
||||
if (hathPerksCookie != null)
|
||||
}
|
||||
if (hathPerksCookie != null) {
|
||||
prefs.eh_hathPerksCookies().set(hathPerksCookie)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -11,9 +11,11 @@ class EhUConfigBuilder {
|
||||
fun build(hathPerks: EHHathPerksResponse): FormBody {
|
||||
val configItems = mutableListOf<ConfigItem>()
|
||||
|
||||
configItems += when (prefs.imageQuality()
|
||||
configItems += when (
|
||||
prefs.imageQuality()
|
||||
.getOrDefault()
|
||||
.toLowerCase()) {
|
||||
.toLowerCase()
|
||||
) {
|
||||
"ovrs_2400" -> Entry.ImageSize.`2400`
|
||||
"ovrs_1600" -> Entry.ImageSize.`1600`
|
||||
"high" -> Entry.ImageSize.`1280`
|
||||
@ -23,20 +25,23 @@ class EhUConfigBuilder {
|
||||
else -> Entry.ImageSize.AUTO
|
||||
}
|
||||
|
||||
configItems += if (prefs.useHentaiAtHome().getOrDefault())
|
||||
configItems += if (prefs.useHentaiAtHome().getOrDefault()) {
|
||||
Entry.UseHentaiAtHome.YES
|
||||
else
|
||||
} else {
|
||||
Entry.UseHentaiAtHome.NO
|
||||
}
|
||||
|
||||
configItems += if (prefs.useJapaneseTitle().getOrDefault())
|
||||
configItems += if (prefs.useJapaneseTitle().getOrDefault()) {
|
||||
Entry.TitleDisplayLanguage.JAPANESE
|
||||
else
|
||||
} else {
|
||||
Entry.TitleDisplayLanguage.DEFAULT
|
||||
}
|
||||
|
||||
configItems += if (prefs.eh_useOriginalImages().getOrDefault())
|
||||
configItems += if (prefs.eh_useOriginalImages().getOrDefault()) {
|
||||
Entry.UseOriginalImages.YES
|
||||
else
|
||||
} else {
|
||||
Entry.UseOriginalImages.NO
|
||||
}
|
||||
|
||||
configItems += when {
|
||||
hathPerks.allThumbs -> Entry.ThumbnailRows.`40`
|
||||
|
@ -14,25 +14,29 @@ class WarnConfigureDialogController : DialogController() {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = "Settings profile note")
|
||||
.message(text = """
|
||||
.title(text = "Settings profile note")
|
||||
.message(
|
||||
text =
|
||||
"""
|
||||
The app will now add a new settings profile on E-Hentai and ExHentai to optimize app performance. Please ensure that you have less than three profiles on both sites.
|
||||
|
||||
If you have no idea what settings profiles are, then it probably doesn't matter, just hit 'OK'.
|
||||
""".trimIndent())
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSettingsUploadWarning().set(false)
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
.cancelable(false)
|
||||
""".trimIndent()
|
||||
)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
prefs.eh_showSettingsUploadWarning().set(false)
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
.cancelable(false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun uploadSettings(router: Router) {
|
||||
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get())
|
||||
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get()) {
|
||||
WarnConfigureDialogController().showDialog(router)
|
||||
else
|
||||
} else {
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,66 +49,68 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
|
||||
val progressSubscriptions = CompositeSubscription()
|
||||
|
||||
presenter.currentlyAddingRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
progressSubscriptions.clear()
|
||||
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
showProgress(this)
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.combineLatest(presenter.progressTotalRelay) { progress, total ->
|
||||
// Show hide dismiss button
|
||||
binding.progressDismissBtn.visibility =
|
||||
if (progress == total)
|
||||
View.VISIBLE
|
||||
else View.GONE
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
progressSubscriptions.clear()
|
||||
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
showProgress(this)
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.combineLatest(presenter.progressTotalRelay) { progress, total ->
|
||||
// Show hide dismiss button
|
||||
binding.progressDismissBtn.visibility =
|
||||
if (progress == total) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
formatProgress(progress, total)
|
||||
}.subscribeUntilDestroy {
|
||||
formatProgress(progress, total)
|
||||
}.subscribeUntilDestroy {
|
||||
binding.progressText.text = it
|
||||
}
|
||||
|
||||
progressSubscriptions += presenter.progressTotalRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.max = it
|
||||
}
|
||||
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.progress = it
|
||||
}
|
||||
|
||||
presenter.eventRelay
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribeUntilDestroy {
|
||||
binding.progressLog.append("$it\n")
|
||||
}?.let {
|
||||
progressSubscriptions += it
|
||||
progressSubscriptions += presenter.progressTotalRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.max = it
|
||||
}
|
||||
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
hideProgress(this)
|
||||
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
||||
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
binding.progressBar.progress = it
|
||||
}
|
||||
|
||||
presenter.eventRelay
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribeUntilDestroy {
|
||||
binding.progressLog.append("$it\n")
|
||||
}?.let {
|
||||
progressSubscriptions += it
|
||||
}
|
||||
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
hideProgress(this)
|
||||
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val View.progressViews
|
||||
get() = listOf(
|
||||
binding.progressTitleView,
|
||||
binding.progressLogWrapper,
|
||||
binding.progressBar,
|
||||
binding.progressText,
|
||||
binding.progressDismissBtn
|
||||
binding.progressTitleView,
|
||||
binding.progressLogWrapper,
|
||||
binding.progressBar,
|
||||
binding.progressText,
|
||||
binding.progressDismissBtn
|
||||
)
|
||||
|
||||
private val View.inputViews
|
||||
get() = listOf(
|
||||
binding.inputTitleView,
|
||||
binding.galleriesBox,
|
||||
binding.btnAddGalleries
|
||||
binding.inputTitleView,
|
||||
binding.galleriesBox,
|
||||
binding.btnAddGalleries
|
||||
)
|
||||
|
||||
private var List<View>.visibility: Int
|
||||
@ -144,12 +146,12 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
|
||||
private fun noGalleriesSpecified() {
|
||||
activity?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "No galleries to add!")
|
||||
.message(text = "You must specify at least one gallery to add!")
|
||||
.positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
.title(text = "No galleries to add!")
|
||||
.message(text = "You must specify at least one gallery to add!")
|
||||
.positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,14 @@ class BatchAddPresenter : BasePresenter<BatchAddController>() {
|
||||
failed.add(s)
|
||||
}
|
||||
progressRelay.call(i + 1)
|
||||
eventRelay?.call((when (result) {
|
||||
is GalleryAddEvent.Success -> "[OK]"
|
||||
is GalleryAddEvent.Fail -> "[ERROR]"
|
||||
}) + " " + result.logMessage)
|
||||
eventRelay?.call(
|
||||
(
|
||||
when (result) {
|
||||
is GalleryAddEvent.Success -> "[OK]"
|
||||
is GalleryAddEvent.Fail -> "[ERROR]"
|
||||
}
|
||||
) + " " + result.logMessage
|
||||
)
|
||||
}
|
||||
|
||||
// Show report
|
||||
|
@ -26,9 +26,9 @@ class AutoSolvingWebViewClient(
|
||||
val doc = response.asJsoup()
|
||||
doc.body().appendChild(Element("script").appendChild(DataNode(CROSS_WINDOW_SCRIPT_INNER)))
|
||||
return WebResourceResponse(
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
|
||||
)
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
|
@ -14,8 +14,9 @@ open class BasicWebViewClient(
|
||||
if (verifyComplete(url)) {
|
||||
activity.finish()
|
||||
} else {
|
||||
if (injectScript != null)
|
||||
if (injectScript != null) {
|
||||
view.evaluateJavascript("(function() {$injectScript})();", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,24 +62,27 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
|
||||
val source = if (originalSource != null) {
|
||||
originalSource as? ActionCompletionVerifier
|
||||
?: run {
|
||||
(originalSource as? HttpSource)?.let {
|
||||
NoopActionCompletionVerifier(it)
|
||||
}
|
||||
?: run {
|
||||
(originalSource as? HttpSource)?.let {
|
||||
NoopActionCompletionVerifier(it)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
val headers = ((source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||
val headers = (
|
||||
(source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()
|
||||
) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||
|
||||
val cookies: HashMap<String, String>? =
|
||||
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
||||
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
||||
|
||||
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE") val verifyComplete = if (source != null) {
|
||||
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE")
|
||||
val verifyComplete = if (source != null) {
|
||||
source::verifyComplete!!
|
||||
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
||||
|
||||
@ -139,10 +142,12 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
webview.webViewClient = if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
||||
// Fetch auto-solve credentials early for speed
|
||||
credentialsObservable = httpClient.newCall(Request.Builder()
|
||||
// Rob demo credentials
|
||||
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
||||
.build())
|
||||
credentialsObservable = httpClient.newCall(
|
||||
Request.Builder()
|
||||
// Rob demo credentials
|
||||
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
||||
.build()
|
||||
)
|
||||
.asObservableSuccess()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map {
|
||||
@ -176,12 +181,12 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
runOnUiThread {
|
||||
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
||||
MaterialDialog(this)
|
||||
.title(text = "Captcha solve failure")
|
||||
.message(text = "Failed to auto-solve the captcha!")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "Captcha solve failure")
|
||||
.message(text = "Failed to auto-solve the captcha!")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,13 +197,19 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
when (stage) {
|
||||
STAGE_CHECKBOX -> {
|
||||
if (result!!.toBoolean()) {
|
||||
webview.postDelayed({
|
||||
getAudioButtonLocation(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
getAudioButtonLocation(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
doStageCheckbox(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
doStageCheckbox(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
||||
@ -216,31 +227,43 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
doStageDownloadAudio(loopId)
|
||||
}
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
getAudioButtonLocation(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
getAudioButtonLocation(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_DOWNLOAD_AUDIO -> {
|
||||
if (result != null) {
|
||||
Timber.d("Got audio URL: $result")
|
||||
performRecognize(result)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
Timber.d("Got audio transcript: $it")
|
||||
webview.post {
|
||||
typeResult(loopId, it!!
|
||||
typeResult(
|
||||
loopId,
|
||||
it!!
|
||||
.replace(TRANSCRIPT_CLEANER_REGEX, "")
|
||||
.replace(SPACE_DEDUPE_REGEX, " ")
|
||||
.trim())
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
captchaSolveFail()
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
doStageDownloadAudio(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
doStageDownloadAudio(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
STAGE_TYPE_RESULT -> {
|
||||
@ -256,27 +279,37 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
fun performRecognize(url: String): Single<String> {
|
||||
return credentialsObservable.flatMap { token ->
|
||||
httpClient.newCall(Request.Builder()
|
||||
httpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.build()).asObservableSuccess().map {
|
||||
.build()
|
||||
).asObservableSuccess().map {
|
||||
token to it
|
||||
}
|
||||
}.flatMap { (token, response) ->
|
||||
val audioFile = response.body!!.bytes()
|
||||
|
||||
httpClient.newCall(Request.Builder()
|
||||
.url("https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!!
|
||||
httpClient.newCall(
|
||||
Request.Builder()
|
||||
.url(
|
||||
"https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("watson-token", token)
|
||||
.build())
|
||||
.post(MultipartBody.Builder()
|
||||
.build()
|
||||
)
|
||||
.post(
|
||||
MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("jsonDescription", RECOGNIZE_JSON)
|
||||
.addFormDataPart("audio.mp3",
|
||||
"audio.mp3",
|
||||
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile))
|
||||
.build())
|
||||
.build()).asObservableSuccess()
|
||||
.addFormDataPart(
|
||||
"audio.mp3",
|
||||
"audio.mp3",
|
||||
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
).asObservableSuccess()
|
||||
}.map { response ->
|
||||
JsonParser.parseString(response.body!!.string())["results"][0]["alternatives"][0]["transcript"].string.trim()
|
||||
}.toSingle()
|
||||
@ -285,7 +318,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
fun doStageCheckbox(loopId: String) {
|
||||
if (loopId != currentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@ -307,11 +341,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback("false", '$loopId', $STAGE_CHECKBOX);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun getAudioButtonLocation(loopId: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@ -339,11 +376,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun doStageDownloadAudio(loopId: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@ -364,11 +404,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback(null, '$loopId', $STAGE_DOWNLOAD_AUDIO);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun typeResult(loopId: String, result: String) {
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@ -392,7 +435,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.callback("false", '$loopId', $STAGE_TYPE_RESULT);
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun beginSolveLoop() {
|
||||
@ -419,12 +464,16 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
} else {
|
||||
val savedStrictValidationStartTime = strictValidationStartTime
|
||||
if (savedStrictValidationStartTime != null &&
|
||||
System.currentTimeMillis() > savedStrictValidationStartTime) {
|
||||
System.currentTimeMillis() > savedStrictValidationStartTime
|
||||
) {
|
||||
captchaSolveFail()
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
runValidateCaptcha(loopId)
|
||||
}, 250)
|
||||
webview.postDelayed(
|
||||
{
|
||||
runValidateCaptcha(loopId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -432,7 +481,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
fun runValidateCaptcha(loopId: String) {
|
||||
if (loopId != validateCurrentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
webview.evaluateJavascript(
|
||||
"""
|
||||
(function() {
|
||||
$CROSS_WINDOW_SCRIPT_OUTER
|
||||
|
||||
@ -453,7 +503,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh.validateCaptchaCallback(false, '$loopId');
|
||||
}
|
||||
})();
|
||||
""".trimIndent().replace("\n", ""), null)
|
||||
""".trimIndent().replace("\n", ""),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun beginValidateCaptchaLoop() {
|
||||
@ -502,7 +554,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
const val STAGE_DOWNLOAD_AUDIO = 2
|
||||
const val STAGE_TYPE_RESULT = 3
|
||||
|
||||
val CROSS_WINDOW_SCRIPT_OUTER = """
|
||||
val CROSS_WINDOW_SCRIPT_OUTER =
|
||||
"""
|
||||
function cwmExec(element, code, cb) {
|
||||
console.log(">>> [CWM-Outer] Running: " + code);
|
||||
let runId = Math.random();
|
||||
@ -523,9 +576,10 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
let runRequest = { id: runId, code: code };
|
||||
element.contentWindow.postMessage("exh-" + JSON.stringify(runRequest), "*");
|
||||
}
|
||||
""".trimIndent().replace("\n", "")
|
||||
""".trimIndent().replace("\n", "")
|
||||
|
||||
val CROSS_WINDOW_SCRIPT_INNER = """
|
||||
val CROSS_WINDOW_SCRIPT_INNER =
|
||||
"""
|
||||
window.addEventListener('message', function(event) {
|
||||
if(typeof event.data === "string" && event.data.startsWith("exh-")) {
|
||||
let request = JSON.parse(event.data.substring(4));
|
||||
@ -538,9 +592,10 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}, false);
|
||||
console.log(">>> [CWM-Inner] Loaded!");
|
||||
alert("exh-");
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val SOLVE_UI_SCRIPT_SHOW = """
|
||||
val SOLVE_UI_SCRIPT_SHOW =
|
||||
"""
|
||||
(function() {
|
||||
let exh_overlay = document.createElement("div");
|
||||
exh_overlay.id = "exh_overlay";
|
||||
@ -568,18 +623,20 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
exh_otext.textContent = "Solving captcha..."
|
||||
document.body.appendChild(exh_otext);
|
||||
})();
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val SOLVE_UI_SCRIPT_HIDE = """
|
||||
val SOLVE_UI_SCRIPT_HIDE =
|
||||
"""
|
||||
(function() {
|
||||
let exh_overlay = document.getElementById("exh_overlay");
|
||||
let exh_otext = document.getElementById("exh_otext");
|
||||
if(exh_overlay != null) exh_overlay.remove();
|
||||
if(exh_otext != null) exh_otext.remove();
|
||||
})();
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val RECOGNIZE_JSON = """
|
||||
val RECOGNIZE_JSON =
|
||||
"""
|
||||
{
|
||||
"part_content_type": "audio/mp3",
|
||||
"keywords": [],
|
||||
@ -596,15 +653,15 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
"customGrammarWords": [],
|
||||
"action": "recognize"
|
||||
}
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
|
||||
val SPACE_DEDUPE_REGEX = Regex(" +")
|
||||
|
||||
private fun baseIntent(context: Context) =
|
||||
Intent(context, BrowserActionActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
Intent(context, BrowserActionActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
fun launchCaptcha(
|
||||
context: Context,
|
||||
@ -689,8 +746,9 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source),
|
||||
ActionCompletionVerifier {
|
||||
class NoopActionCompletionVerifier(private val source: HttpSource) :
|
||||
DelegatedHttpSource(source),
|
||||
ActionCompletionVerifier {
|
||||
override val versionId get() = source.versionId
|
||||
override val lang: String get() = source.lang
|
||||
|
||||
|
@ -37,43 +37,43 @@ open class HeadersInjectingWebViewClient(
|
||||
|
||||
companion object {
|
||||
private val FALLBACK_REASON_PHRASES = mapOf(
|
||||
100 to "Continue",
|
||||
101 to "Switching Protocols",
|
||||
200 to "OK",
|
||||
201 to "Created",
|
||||
202 to "Accepted",
|
||||
203 to "Non-Authoritative Information",
|
||||
204 to "No Content",
|
||||
205 to "Reset Content",
|
||||
206 to "Partial Content",
|
||||
300 to "Multiple Choices",
|
||||
301 to "Moved Permanently",
|
||||
302 to "Moved Temporarily",
|
||||
303 to "See Other",
|
||||
304 to "Not Modified",
|
||||
305 to "Use Proxy",
|
||||
400 to "Bad Request",
|
||||
401 to "Unauthorized",
|
||||
402 to "Payment Required",
|
||||
403 to "Forbidden",
|
||||
404 to "Not Found",
|
||||
405 to "Method Not Allowed",
|
||||
406 to "Not Acceptable",
|
||||
407 to "Proxy Authentication Required",
|
||||
408 to "Request Time-out",
|
||||
409 to "Conflict",
|
||||
410 to "Gone",
|
||||
411 to "Length Required",
|
||||
412 to "Precondition Failed",
|
||||
413 to "Request Entity Too Large",
|
||||
414 to "Request-URI Too Large",
|
||||
415 to "Unsupported Media Type",
|
||||
500 to "Internal Server Error",
|
||||
501 to "Not Implemented",
|
||||
502 to "Bad Gateway",
|
||||
503 to "Service Unavailable",
|
||||
504 to "Gateway Time-out",
|
||||
505 to "HTTP Version not supported"
|
||||
100 to "Continue",
|
||||
101 to "Switching Protocols",
|
||||
200 to "OK",
|
||||
201 to "Created",
|
||||
202 to "Accepted",
|
||||
203 to "Non-Authoritative Information",
|
||||
204 to "No Content",
|
||||
205 to "Reset Content",
|
||||
206 to "Partial Content",
|
||||
300 to "Multiple Choices",
|
||||
301 to "Moved Permanently",
|
||||
302 to "Moved Temporarily",
|
||||
303 to "See Other",
|
||||
304 to "Not Modified",
|
||||
305 to "Use Proxy",
|
||||
400 to "Bad Request",
|
||||
401 to "Unauthorized",
|
||||
402 to "Payment Required",
|
||||
403 to "Forbidden",
|
||||
404 to "Not Found",
|
||||
405 to "Method Not Allowed",
|
||||
406 to "Not Acceptable",
|
||||
407 to "Proxy Authentication Required",
|
||||
408 to "Request Time-out",
|
||||
409 to "Conflict",
|
||||
410 to "Gone",
|
||||
411 to "Length Required",
|
||||
412 to "Precondition Failed",
|
||||
413 to "Request Entity Too Large",
|
||||
414 to "Request-URI Too Large",
|
||||
415 to "Unsupported Media Type",
|
||||
500 to "Internal Server Error",
|
||||
501 to "Not Implemented",
|
||||
502 to "Bad Gateway",
|
||||
503 to "Service Unavailable",
|
||||
504 to "Gateway Time-out",
|
||||
505 to "HTTP Version not supported"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import okhttp3.Request
|
||||
|
||||
fun WebResourceRequest.toOkHttpRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.method(method, null)
|
||||
.url(url.toString())
|
||||
.method(method, null)
|
||||
|
||||
requestHeaders.entries.forEach { (t, u) ->
|
||||
request.addHeader(t, u)
|
||||
|
@ -54,33 +54,35 @@ class InterceptActivity : BaseRxActivity<EhActivityInterceptBinding, InterceptAc
|
||||
super.onStart()
|
||||
statusSubscription?.unsubscribe()
|
||||
statusSubscription = presenter.status
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is InterceptResult.Success -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Launching app..."
|
||||
onBackPressed()
|
||||
startActivity(Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, it.mangaId))
|
||||
}
|
||||
is InterceptResult.Failure -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Error: ${it.reason}"
|
||||
MaterialDialog(this)
|
||||
.title(text = "Error")
|
||||
.message(text = "Could not open this gallery:\n\n${it.reason}")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.onCancel { onBackPressed() }
|
||||
.onDismiss { onBackPressed() }
|
||||
.show()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is InterceptResult.Success -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Launching app..."
|
||||
onBackPressed()
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(MangaController.MANGA_EXTRA, it.mangaId)
|
||||
)
|
||||
}
|
||||
is InterceptResult.Failure -> {
|
||||
binding.interceptProgress.gone()
|
||||
binding.interceptStatus.text = "Error: ${it.reason}"
|
||||
MaterialDialog(this)
|
||||
.title(text = "Error")
|
||||
.message(text = "Could not open this gallery:\n\n${it.reason}")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.onCancel { onBackPressed() }
|
||||
.onDismiss { onBackPressed() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -21,12 +21,14 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||
thread {
|
||||
val result = galleryAdder.addGallery(gallery)
|
||||
|
||||
status.onNext(when (result) {
|
||||
is GalleryAddEvent.Success -> result.manga.id?.let {
|
||||
InterceptResult.Success(it)
|
||||
} ?: InterceptResult.Failure("Manga ID is null!")
|
||||
is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage)
|
||||
})
|
||||
status.onNext(
|
||||
when (result) {
|
||||
is GalleryAddEvent.Success -> result.manga.id?.let {
|
||||
InterceptResult.Success(it)
|
||||
} ?: InterceptResult.Failure("Manga ID is null!")
|
||||
is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,18 +25,18 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
val fingerprintSupported
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered()
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered()
|
||||
|
||||
val useFingerprint
|
||||
get() = fingerprintSupported &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onAttached() {
|
||||
@ -44,29 +44,32 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
if (fingerprintSupported) {
|
||||
updateSummary()
|
||||
onChange {
|
||||
if (it as Boolean)
|
||||
if (it as Boolean) {
|
||||
tryChange()
|
||||
else
|
||||
} else {
|
||||
prefs.eh_lockUseFingerprint().set(false)
|
||||
}
|
||||
!it
|
||||
}
|
||||
} else {
|
||||
title = "Fingerprint unsupported"
|
||||
shouldDisableView = true
|
||||
summary = if (!Reprint.hasFingerprintRegistered())
|
||||
summary = if (!Reprint.hasFingerprintRegistered()) {
|
||||
"No fingerprints enrolled!"
|
||||
else
|
||||
} else {
|
||||
"Fingerprint unlock is unsupported on this device!"
|
||||
}
|
||||
onChange { false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSummary() {
|
||||
isChecked = useFingerprint
|
||||
title = if (isChecked)
|
||||
title = if (isChecked) {
|
||||
"Fingerprint enabled"
|
||||
else
|
||||
} else {
|
||||
"Fingerprint disabled"
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@ -74,9 +77,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
val statusTextView = TextView(context).apply {
|
||||
text = "Please touch the fingerprint sensor"
|
||||
val size = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
setPadding(0, 0, dpToPx(context, 8), 0)
|
||||
@ -84,9 +89,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
}
|
||||
val iconView = SwirlView(context).apply {
|
||||
val size = dpToPx(context, 30)
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
}
|
||||
@ -96,9 +103,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
orientation = LinearLayoutCompat.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT
|
||||
layoutParams = (layoutParams ?: LinearLayoutCompat.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: LinearLayoutCompat.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
val pSize = dpToPx(context, 24)
|
||||
@ -109,39 +118,39 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
addView(iconView)
|
||||
}
|
||||
val dialog = MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification")
|
||||
.customView(view = linearLayout)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.title(text = "Fingerprint verification")
|
||||
.customView(view = linearLayout)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
dialog.show()
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
val subscription = RxReprint.authenticate()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
when (result.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> {
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
prefs.eh_lockUseFingerprint().set(true)
|
||||
dialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> {
|
||||
iconView.setState(SwirlView.State.ERROR)
|
||||
statusTextView.text = result.errorMessage
|
||||
}
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification failed!")
|
||||
.message(text = result.errorMessage)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
when (result.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> {
|
||||
iconView.setState(SwirlView.State.ON)
|
||||
prefs.eh_lockUseFingerprint().set(true)
|
||||
dialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> {
|
||||
iconView.setState(SwirlView.State.ERROR)
|
||||
statusTextView.text = result.errorMessage
|
||||
}
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint verification failed!")
|
||||
.message(text = result.errorMessage)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
|
@ -20,19 +20,21 @@ object LockActivityDelegate {
|
||||
private val uiScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
fun doLock(router: Router, animate: Boolean = false) {
|
||||
router.pushController(RouterTransaction.with(LockController())
|
||||
.popChangeHandler(LockChangeHandler(animate)))
|
||||
router.pushController(
|
||||
RouterTransaction.with(LockController())
|
||||
.popChangeHandler(LockChangeHandler(animate))
|
||||
)
|
||||
}
|
||||
|
||||
fun onCreate(activity: FragmentActivity) {
|
||||
preferences.secureScreen().asFlow()
|
||||
.onEach {
|
||||
if (it) {
|
||||
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
.onEach {
|
||||
if (it) {
|
||||
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
.launchIn(uiScope)
|
||||
}
|
||||
|
||||
|
@ -35,5 +35,5 @@ class LockChangeHandler : AnimatorChangeHandler {
|
||||
override fun resetFromView(from: View) {}
|
||||
|
||||
override fun copy(): ControllerChangeHandler =
|
||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
}
|
||||
|
@ -53,12 +53,12 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
closeLock()
|
||||
} else {
|
||||
MaterialDialog(context)
|
||||
.title(text = "PIN code incorrect")
|
||||
.message(text = "The PIN code you entered is incorrect. Please try again.")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
.title(text = "PIN code incorrect")
|
||||
.message(text = "The PIN code you entered is incorrect. Please try again.")
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
binding.pinLockView.resetPinLockView()
|
||||
}
|
||||
}
|
||||
@ -79,9 +79,11 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
binding.swirlContainer.removeAllViews()
|
||||
val icon = SwirlView(context).apply {
|
||||
val size = dpToPx(context, 60)
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(
|
||||
layoutParams = (
|
||||
layoutParams ?: ViewGroup.LayoutParams(
|
||||
size, size
|
||||
)).apply {
|
||||
)
|
||||
).apply {
|
||||
width = size
|
||||
height = size
|
||||
|
||||
@ -92,29 +94,30 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
|
||||
setBackgroundColor(lockColor)
|
||||
val bgColor = resolvColor(android.R.attr.colorBackground)
|
||||
// Disable elevation if lock color is same as background color
|
||||
if (lockColor == bgColor)
|
||||
if (lockColor == bgColor) {
|
||||
this@with.swirl_container.cardElevation = 0f
|
||||
}
|
||||
setState(SwirlView.State.OFF, true)
|
||||
}
|
||||
binding.swirlContainer.addView(icon)
|
||||
icon.setState(SwirlView.State.ON)
|
||||
RxReprint.authenticate()
|
||||
.subscribeUntilDetach {
|
||||
when (it.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> closeLock()
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint error!")
|
||||
.message(text = it.errorMessage)
|
||||
.cancelable(false)
|
||||
.cancelOnTouchOutside(false)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
icon.setState(SwirlView.State.OFF)
|
||||
}
|
||||
.subscribeUntilDetach {
|
||||
when (it.status) {
|
||||
AuthenticationResult.Status.SUCCESS -> closeLock()
|
||||
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
|
||||
AuthenticationResult.Status.FATAL_FAILURE, null -> {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Fingerprint error!")
|
||||
.message(text = it.errorMessage)
|
||||
.cancelable(false)
|
||||
.cancelOnTouchOutside(false)
|
||||
.positiveButton(android.R.string.ok)
|
||||
.show()
|
||||
icon.setState(SwirlView.State.OFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.swirlContainer.visibility = View.GONE
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
private val secureRandom by lazy { SecureRandom() }
|
||||
|
||||
@ -46,28 +46,28 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
fun tryChange() {
|
||||
if (!notifyLockSecurity(context)) {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Lock application")
|
||||
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
// .inputRangeRes(0, 10, R.color.material_red_500)
|
||||
// .inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
|
||||
val progressDialog = MaterialDialog(context)
|
||||
.title(text = "Saving password")
|
||||
.cancelable(false)
|
||||
progressDialog.show()
|
||||
Observable.fromCallable {
|
||||
savePassword(c.toString())
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
}
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
.title(text = "Lock application")
|
||||
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
// .inputRangeRes(0, 10, R.color.material_red_500)
|
||||
// .inputType(InputType.TYPE_CLASS_NUMBER)
|
||||
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
|
||||
val progressDialog = MaterialDialog(context)
|
||||
.title(text = "Saving password")
|
||||
.cancelable(false)
|
||||
progressDialog.show()
|
||||
Observable.fromCallable {
|
||||
savePassword(c.toString())
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
progressDialog.dismiss()
|
||||
updateSummary()
|
||||
}
|
||||
}
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ class LockPresenter : BasePresenter<LockController>() {
|
||||
|
||||
val useFingerprint
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered() &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered() &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
}
|
||||
|
@ -39,8 +39,8 @@ fun sha512(passwordToHash: String, salt: String): String {
|
||||
*/
|
||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) =
|
||||
prefs.eh_lockHash().get() != null &&
|
||||
prefs.eh_lockSalt().get() != null &&
|
||||
prefs.eh_lockLength().getOrDefault() != -1
|
||||
prefs.eh_lockSalt().get() != null &&
|
||||
prefs.eh_lockLength().getOrDefault() != -1
|
||||
|
||||
/**
|
||||
* Check if the lock will function properly
|
||||
@ -53,30 +53,35 @@ fun notifyLockSecurity(
|
||||
): Boolean {
|
||||
return false
|
||||
if (!prefs.eh_lockManually().getOrDefault() &&
|
||||
!hasAccessToUsageStats(context)) {
|
||||
!hasAccessToUsageStats(context)
|
||||
) {
|
||||
MaterialDialog(context)
|
||||
.title(text = "Permission required")
|
||||
.message(text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
"This is required for the application lock to function properly. " +
|
||||
"Press OK to grant this permission now.")
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
|
||||
MaterialDialog(context)
|
||||
.title(text = "Grant permission manually")
|
||||
.message(text = "Failed to launch the window used to grant the usage stats permission. " +
|
||||
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'.")
|
||||
.positiveButton(android.R.string.ok) { it.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
}
|
||||
.title(text = "Permission required")
|
||||
.message(
|
||||
text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
"This is required for the application lock to function properly. " +
|
||||
"Press OK to grant this permission now."
|
||||
)
|
||||
.negativeButton(R.string.action_cancel)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
|
||||
MaterialDialog(context)
|
||||
.title(text = "Grant permission manually")
|
||||
.message(
|
||||
text = "Failed to launch the window used to grant the usage stats permission. " +
|
||||
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'."
|
||||
)
|
||||
.positiveButton(android.R.string.ok) { it.dismiss() }
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(false)
|
||||
.show()
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
@ -97,10 +97,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
val parsedUrl = Uri.parse(url)
|
||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT))
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
|
||||
}
|
||||
// Check login result
|
||||
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
}
|
||||
@ -128,9 +129,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
fun checkLoginCookies(url: String): Boolean {
|
||||
getCookies(url)?.let { parsed ->
|
||||
return parsed.filter {
|
||||
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
|
||||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)) &&
|
||||
it.value.isNotBlank()
|
||||
(
|
||||
it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
|
||||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)
|
||||
) &&
|
||||
it.value.isNotBlank()
|
||||
}.count() >= 2
|
||||
}
|
||||
return false
|
||||
@ -168,11 +171,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
}
|
||||
|
||||
fun getCookies(url: String): List<HttpCookie>? =
|
||||
CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PARAM_SKIP_INJECT = "TEH_SKIP_INJECT"
|
||||
@ -181,7 +184,8 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
|
||||
const val PASS_HASH_COOKIE = "ipb_pass_hash"
|
||||
const val IGNEOUS_COOKIE = "igneous"
|
||||
|
||||
const val HIDE_JS = """
|
||||
const val HIDE_JS =
|
||||
"""
|
||||
javascript:(function () {
|
||||
document.getElementsByTagName('body')[0].style.visibility = 'hidden';
|
||||
document.getElementsByName('submit')[0].style.visibility = 'visible';
|
||||
|
@ -16,7 +16,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: SourceController.SmartSearchConfig?) :
|
||||
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||
|
||||
override val coroutineContext = Job() + Dispatchers.Main
|
||||
|
||||
|
@ -14,16 +14,16 @@ import java.util.Date
|
||||
|
||||
inline fun <reified E : RealmModel> RealmQuery<out E>.beginLog(
|
||||
clazz: Class<out E>? =
|
||||
E::class.java
|
||||
E::class.java
|
||||
): LoggingRealmQuery<out E> =
|
||||
LoggingRealmQuery.fromQuery(this, clazz)
|
||||
|
||||
class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
companion object {
|
||||
fun <E : RealmModel> fromQuery(q: RealmQuery<out E>, clazz: Class<out E>?) =
|
||||
LoggingRealmQuery(q).apply {
|
||||
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
|
||||
}
|
||||
LoggingRealmQuery(q).apply {
|
||||
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
|
||||
}
|
||||
}
|
||||
|
||||
private val log = mutableListOf<String>()
|
||||
@ -47,9 +47,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendEqualTo(fieldName: String, value: String, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" == \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" == \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun equalTo(fieldName: String, value: String): RealmQuery<E> {
|
||||
@ -108,11 +112,18 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
fun appendIn(fieldName: String, values: Array<out Any?>, casing: Case? = null) {
|
||||
log += sec("[${values.joinToString(separator = ", ", transform = {
|
||||
"\"$it\""
|
||||
})}] IN \"$fieldName\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"[${values.joinToString(
|
||||
separator = ", ",
|
||||
transform = {
|
||||
"\"$it\""
|
||||
}
|
||||
)}] IN \"$fieldName\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun `in`(fieldName: String, values: Array<String>): RealmQuery<E> {
|
||||
@ -166,9 +177,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendNotEqualTo(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" != \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" != \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun notEqualTo(fieldName: String, value: String): RealmQuery<E> {
|
||||
@ -372,9 +387,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendContains(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" CONTAINS \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" CONTAINS \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun contains(fieldName: String, value: String): RealmQuery<E> {
|
||||
@ -388,9 +407,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendBeginsWith(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" BEGINS WITH \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" BEGINS WITH \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun beginsWith(fieldName: String, value: String): RealmQuery<E> {
|
||||
@ -404,9 +427,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendEndsWith(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" ENDS WITH \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" ENDS WITH \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun endsWith(fieldName: String, value: String): RealmQuery<E> {
|
||||
@ -420,9 +447,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
|
||||
}
|
||||
|
||||
private fun appendLike(fieldName: String, value: Any?, casing: Case? = null) {
|
||||
log += sec("\"$fieldName\" LIKE \"$value\"" + (casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""))
|
||||
log += sec(
|
||||
"\"$fieldName\" LIKE \"$value\"" + (
|
||||
casing?.let {
|
||||
" CASE ${casing.name}"
|
||||
} ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun like(fieldName: String, value: String): RealmQuery<E> {
|
||||
|
@ -209,10 +209,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val entries: Set<Map.Entry<String, T>>
|
||||
get() {
|
||||
val out = mutableSetOf<Map.Entry<String, T>>()
|
||||
node.walk("", { k, v ->
|
||||
out.add(AbstractMap.SimpleImmutableEntry(k, v))
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ k, v ->
|
||||
out.add(AbstractMap.SimpleImmutableEntry(k, v))
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
/**
|
||||
@ -221,10 +225,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val keys: Set<String>
|
||||
get() {
|
||||
val out = mutableSetOf<String>()
|
||||
node.walk("", { k, _ ->
|
||||
out.add(k)
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ k, _ ->
|
||||
out.add(k)
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
@ -243,10 +251,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
override val values: Collection<T>
|
||||
get() {
|
||||
val out = mutableSetOf<T>()
|
||||
node.walk("", { _, v ->
|
||||
out.add(v)
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ _, v ->
|
||||
out.add(v)
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
@ -264,10 +276,14 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
* Returns `true` if the map maps one or more keys to the specified [value].
|
||||
*/
|
||||
override fun containsValue(value: T): Boolean {
|
||||
node.walk("", { _, v ->
|
||||
if (v == value) return true
|
||||
true
|
||||
}, leavesOnly)
|
||||
node.walk(
|
||||
"",
|
||||
{ _, v ->
|
||||
if (v == value) return true
|
||||
true
|
||||
},
|
||||
leavesOnly
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -315,32 +331,38 @@ class NakedTrie<T> : MutableMap<String, T> {
|
||||
* Returns a [MutableSet] of all key/value pairs in this map.
|
||||
*/
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<String, T>>
|
||||
get() = FakeMutableSet.fromSet(mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
|
||||
walk { k, v ->
|
||||
this += FakeMutableEntry.fromPair(k, v)
|
||||
true
|
||||
get() = FakeMutableSet.fromSet(
|
||||
mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
|
||||
walk { k, v ->
|
||||
this += FakeMutableEntry.fromPair(k, v)
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [MutableSet] of all keys in this map.
|
||||
*/
|
||||
override val keys: MutableSet<String>
|
||||
get() = FakeMutableSet.fromSet(mutableSetOf<String>().apply {
|
||||
walk { k, _ ->
|
||||
this += k
|
||||
true
|
||||
get() = FakeMutableSet.fromSet(
|
||||
mutableSetOf<String>().apply {
|
||||
walk { k, _ ->
|
||||
this += k
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [MutableCollection] of all values in this map. Note that this collection may contain duplicate values.
|
||||
*/
|
||||
override val values: MutableCollection<T>
|
||||
get() = FakeMutableCollection.fromCollection(mutableListOf<T>().apply {
|
||||
walk { _, v ->
|
||||
this += v
|
||||
true
|
||||
get() = FakeMutableCollection.fromCollection(
|
||||
mutableListOf<T>().apply {
|
||||
walk { _, v ->
|
||||
this += v
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -9,11 +9,12 @@ import org.jsoup.nodes.Document
|
||||
fun Response.interceptAsHtml(block: (Document) -> Unit): Response {
|
||||
val body = body
|
||||
if (body?.contentType()?.type == "text" &&
|
||||
body.contentType()?.subtype == "html") {
|
||||
body.contentType()?.subtype == "html"
|
||||
) {
|
||||
val bodyString = body.string()
|
||||
val rebuiltResponse = newBuilder()
|
||||
.body(ResponseBody.create(body.contentType(), bodyString))
|
||||
.build()
|
||||
.body(ResponseBody.create(body.contentType(), bodyString))
|
||||
.build()
|
||||
try {
|
||||
// Search for captcha
|
||||
val parsed = asJsoup(html = bodyString)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user