Linting Fixes AZ

This commit is contained in:
Jobobby04
2020-05-02 00:46:24 -04:00
parent 03e5c5ca10
commit 7e99a9f789
108 changed files with 2962 additions and 2412 deletions

View File

@ -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),
return DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
cacheSize * 1024 * 1024)
cacheSize * 1024 * 1024
)
}
// <-- EH

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
@ -122,14 +124,16 @@ class EHentai(
thumbnail_url = thumbnailElement.attr("src")
// 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
@ -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 ->
date_upload = EX_DATE_FORMAT.parse(
d.select("#gdd .gdt1").find { el ->
el.text().toLowerCase() == "posted:"
}!!.nextElementSibling().text()).time
}!!.nextElementSibling().text()
).time
}
// Build and append the rest of the galleries
if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self)
@ -253,17 +259,22 @@ 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) =
@ -314,9 +325,12 @@ 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 {
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 {
} ?: url,
additionalHeaders?.let {
val headers = headers.newBuilder()
it.toMultimap().forEach { (t, u) ->
u.forEach {
@ -324,12 +338,15 @@ class EHentai(
}
}
headers.build()
} ?: headers).let {
if (!cache)
} ?: headers
).let {
if (!cache) {
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
else
} else {
it
}!!
}
}
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
@ -352,9 +369,13 @@ class EHentai(
} else Observable.just(doc)
pre.flatMap {
parseToManga(manga, it).andThen(Observable.just(manga.apply {
parseToManga(manga, it).andThen(
Observable.just(
manga.apply {
initialized = true
}))
}
)
)
}
} else {
response.close()
@ -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)
}
@ -452,16 +476,19 @@ class EHentai(
tags.clear()
select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":")
tags.addAll(it.select("div").map { element ->
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
@ -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,17 +574,20 @@ 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)
cookies["sl"] = "dm_2"
@ -595,13 +631,17 @@ class EHentai(
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(
class GenreGroup :
Filter.Group<GenreOption>(
"Genres",
listOf(
GenreOption("Dōjinshi", 2),
GenreOption("Manga", 4),
GenreOption("Artist CG", 8),
@ -612,7 +652,9 @@ class EHentai(
GenreOption("Cosplay", 64),
GenreOption("Asian Porn", 128),
GenreOption("Misc", 1)
)), UriFilter {
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) {
val bits = state.fold(0) { acc, genre ->
if (!genre.state) acc + genre.genreId else acc
@ -623,10 +665,11 @@ 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")
}
}
}
open class PageOption(name: String, private val queryKey: String) : Filter.Text(name), UriFilter {
override fun addToUri(builder: Uri.Builder) {
@ -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(
class RatingOption :
Filter.Select<String>(
"Minimum Rating",
arrayOf(
"Any",
"2 stars",
"3 stars",
"4 stars",
"5 stars"
)), UriFilter {
)
),
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,14 +720,16 @@ 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)
@ -717,17 +769,23 @@ class EHentai(
val json = JsonObject()
json["method"] = "gtoken"
json["pagelist"] = JsonArray().apply {
add(JsonArray().apply {
add(
JsonArray().apply {
add(gallery.toInt())
add(pageToken)
add(pageNum.toInt())
})
}
)
}
val outJson = JsonParser.parseString(client.newCall(Request.Builder()
val outJson = JsonParser.parseString(
client.newCall(
Request.Builder()
.url(EH_API_BASE)
.post(RequestBody.create(JSON, json.toString()))
.build()).execute().body!!.string()).obj
.build()
).execute().body!!.string()
).obj
val obj = outJson["tokenlist"].array.first()
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"

View File

@ -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()
@ -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 {
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 {
@ -320,9 +324,13 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}))
}
)
)
}
}
@ -407,8 +415,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
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)
}
}
}
}

View File

@ -136,9 +136,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it).andThen(Observable.just(manga.apply {
parseToManga(manga, it).andThen(
Observable.just(
manga.apply {
initialized = true
}))
}
)
)
}
}
@ -208,11 +212,12 @@ 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)))
@ -220,19 +225,25 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
.toSingle()
}
override fun fetchChapterList(manga: SManga) = Observable.just(listOf(SChapter.create().apply {
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!!)!!
@ -270,12 +281,14 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
fun nhGet(url: String, tag: Any? = null) = GET(url)
.newBuilder()
.header("User-Agent",
.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}")
"$appName/${BuildConfig.VERSION_CODE}"
)
.tag(tag).build()
override val id = NHENTAI_SOURCE_ID
@ -295,8 +308,9 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
)
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]}/"
}

View File

@ -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
*/
@ -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/")
}
@ -131,9 +135,13 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}))
}
)
)
}
}
@ -165,10 +173,11 @@ 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" -> {
if (it is Element && it.tagName() == "a") {
artist = it.text()
@ -176,26 +185,29 @@ 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
}
}
}
}
}
}
altTitles = newAltTitles
@ -227,7 +239,8 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
this,
SManga.create().apply {
title = ""
})
}
)
try {
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
@ -249,30 +262,42 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
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(
class ReleaseYearRangeFilter :
Filter.Select<String>(
"Range",
arrayOf(
"on",
"after",
"before"
)), UriFilter {
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("releasedType", state.toString())
}
@ -296,20 +321,24 @@ 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())
}
}
}
override val matchingHosts = listOf("www.perveden.com")

View File

@ -41,7 +41,8 @@ import rx.schedulers.Schedulers
typealias SiteMap = NakedTrie<Unit>
class EightMuses : HttpSource(),
class EightMuses :
HttpSource(),
LewdSource<EightMusesSearchMetadata, Document>,
UrlImportableSource {
override val id = EIGHTMUSES_SOURCE_ID
@ -184,9 +185,11 @@ class EightMuses : HttpSource(),
return client.newCall(request)
.asObservableSuccess()
.flatMapSingle { response ->
RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) {
RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
parseResultsPage(response, dig)
}.asSingle(GlobalScope.coroutineContext))
}.asSingle(GlobalScope.coroutineContext)
)
}
}
@ -259,9 +262,11 @@ class EightMuses : HttpSource(),
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) {
return RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
fetchAndParseChapterList("", manga.url)
}.asSingle(GlobalScope.coroutineContext)).toObservable()
}.asSingle(GlobalScope.coroutineContext)
).toObservable()
}
private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {

View File

@ -132,7 +132,8 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Should not be called!")
private fun fetchSearchMangaInternal(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) {
return RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
val modeFilter = filters.filterIsInstance<ModeFilter>().firstOrNull()
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()
@ -196,10 +197,12 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
base += "/$page"
if (isSortFilter) {
parseListing(client.newCall(GET(baseUrl + base, headers))
parseListing(
client.newCall(GET(baseUrl + base, headers))
.asObservableSuccess()
.toSingle()
.await(Schedulers.io()))
.await(Schedulers.io())
)
} else {
val body = if (tagQuery != null) {
FormBody.Builder()
@ -224,17 +227,22 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
.toSingle()
.await(Schedulers.io())
if (!processResponse.isRedirect)
if (!processResponse.isRedirect) {
throw IllegalStateException("Unexpected process response code!")
}
val sessId = processResponse.headers("Set-Cookie").find {
it.startsWith("PHPSESSID")
} ?: throw IllegalStateException("Missing server session cookie!")
val response = clientWithoutCookies.newCall(GET(baseUrl + base,
val response = clientWithoutCookies.newCall(
GET(
baseUrl + base,
headersBuilder()
.set("Cookie", BASE_COOKIES + " " + sessId.substringBefore(';'))
.build()))
.build()
)
)
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
@ -254,7 +262,8 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
hasNextPage
)
}
}.asSingle(GlobalScope.coroutineContext)).toObservable()
}.asSingle(GlobalScope.coroutineContext)
).toObservable()
}
// Collection must be sorted and cannot be sorted
@ -397,7 +406,10 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
return emptyList()
}
class HelpFilter : Filter.HelpDialog("Usage instructions", markdown = """
class HelpFilter : Filter.HelpDialog(
"Usage instructions",
markdown =
"""
### Modes
There are three available filter modes:
- Text search
@ -416,12 +428,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
View a list of all galleries sorted by a specific parameter. Exit sorting mode by resetting the filters using the reset button near the bottom of the screen.
### Tag list
""".trimIndent() + "\n$TAGS_AS_MARKDOWN")
""".trimIndent() + "\n$TAGS_AS_MARKDOWN"
)
class ModeFilter : Filter.Select<String>("Mode", arrayOf(
class ModeFilter : Filter.Select<String>(
"Mode",
arrayOf(
"Text search",
"Tag search"
))
)
)
class SortFilter : Filter.Sort("Sort", SORT_OPTIONS.map { it.second }.toTypedArray()) {
companion object {

View File

@ -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).
*/
@ -40,9 +42,13 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}))
}
)
)
}
}
@ -98,9 +104,10 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
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"
}
}
}

View File

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

View File

@ -19,8 +19,10 @@ 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"
@ -32,8 +34,9 @@ class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
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}"
}
@ -106,9 +109,11 @@ class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
character = newCharacter
input.getElementById("Tag")?.children()?.let {
tags.addAll(it.map {
tags.addAll(
it.map {
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
})
}
)
}
}
}

View File

@ -131,10 +131,14 @@ class SourceController :
// Open the catalogue view.
openCatalogue(source, BrowseSourceController(source))
}
Mode.SMART_SEARCH -> router.pushController(SmartSearchController(Bundle().apply {
Mode.SMART_SEARCH -> router.pushController(
SmartSearchController(
Bundle().apply {
putLong(SmartSearchController.ARG_SOURCE_ID, source.id)
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig)
}).withFadeTransaction())
}
).withFadeTransaction()
)
}
return false
}

View File

@ -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,
EXHSavedSearch(
content["name"].string,
content["query"].string,
originalFilters)
originalFilters
)
} catch (t: RuntimeException) {
// Load failed
Timber.e(t, "Failed to load saved search!")

View File

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

View File

@ -185,16 +185,20 @@ class MangaInfoPresenter(
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
@ -203,7 +207,8 @@ class MangaInfoPresenter(
manga.source,
manga.url
)
))
)
)
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
copyFrom(originalManga)
favorite = true

View File

@ -23,18 +23,23 @@ 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)
}
}

View File

@ -33,8 +33,10 @@ class MigrationBottomSheetDialog(
private val listener:
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
)
}
}
}

View File

@ -16,9 +16,14 @@ class MigrationSourceAdapter(
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(SELECTED_SOURCES_KEY, ArrayList(currentItems.map {
outState.putParcelableArrayList(
SELECTED_SOURCES_KEY,
ArrayList(
currentItems.map {
it.asParcelable()
}))
}
)
)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {

View File

@ -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,8 +142,11 @@ 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.filter { isEnabled(it.id.toString()) }.sortedBy {
sourcesSaved.indexOf(
it.id
.toString()
)
} +
sources.filterNot { isEnabled(it.id.toString()) }
@ -167,9 +176,11 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
}
fun create(mangaIds: List<Long>): PreMigrationController {
return PreMigrationController(Bundle().apply {
return PreMigrationController(
Bundle().apply {
putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray())
})
}
)
}
}
}

View File

@ -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 {
return MigrationListController(
Bundle().apply {
putParcelable(CONFIG_EXTRA, config)
})
}
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -45,22 +45,28 @@ object EXHMigrations {
if (oldVersion < 1) {
db.inTransaction {
// Migrate HentaiCafe source IDs
db.lowLevel().executeSQL(RawQuery.builder()
.query("""
db.lowLevel().executeSQL(
RawQuery.builder()
.query(
"""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 6908
""".trimIndent())
""".trimIndent()
)
.affectsTables(MangaTable.TABLE)
.build())
.build()
)
// Migrate nhentai URLs
val nhentaiManga = db.db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
.build())
.build()
)
.prepare()
.executeAsBlocking()
@ -85,14 +91,18 @@ object EXHMigrations {
if (oldVersion < 8405) {
db.inTransaction {
// Migrate HBrowse source IDs
db.lowLevel().executeSQL(RawQuery.builder()
.query("""
db.lowLevel().executeSQL(
RawQuery.builder()
.query(
"""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
""".trimIndent())
""".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("""
db.lowLevel().executeSQL(
RawQuery.builder()
.query(
"""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 6909
""".trimIndent())
""".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

View File

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

View File

@ -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("""
db.lowLevel().executeSQL(
RawQuery.builder()
.query(
"""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_FAVORITE} = 1
""".trimIndent())
""".trimIndent()
)
.affectsTables(MangaTable.TABLE)
.build())
.build()
)
}
}
@ -110,13 +115,17 @@ object DebugFunctions {
fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()
private fun convertSources(from: Long, to: Long) {
db.lowLevel().executeSQL(RawQuery.builder()
.query("""
db.lowLevel().executeSQL(
RawQuery.builder()
.query(
"""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_SOURCE} = $to
WHERE ${MangaTable.COL_SOURCE} = $from
""".trimIndent())
""".trimIndent()
)
.affectsTables(MangaTable.TABLE)
.build())
.build()
)
}
}

View File

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

View File

@ -30,9 +30,11 @@ 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 ->
val chainsObservable = Observable.merge(
chapters.map { chapter ->
db.getChapters(chapter.url).asRxSingle().toObservable()
}).toList().map { allChapters ->
}
).toList().map { allChapters ->
allChapters.flatMap { innerChapters -> innerChapters.map { it.manga_id!! } }.distinct()
}.flatMap { mangaIds ->
Observable.merge(
@ -77,7 +79,8 @@ class EHentaiUpdateHelper(context: Context) {
// Convert old style chapters to new style chapters if possible
if (chapter.date_upload <= 0 &&
meta?.datePosted != null &&
meta?.title != null) {
meta?.title != null
) {
chapter.name = meta!!.title!!
chapter.date_upload = meta!!.datePosted!!
}

View File

@ -137,8 +137,9 @@ 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
@ -146,8 +147,9 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
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)...",
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)
modifiedThisIteration.size
)
if (manga.id in modifiedThisIteration) {
// We already processed this manga!
@ -194,22 +198,26 @@ 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)",
logger.e(
"> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
manga.id,
meta.gId,
meta.gToken,
failuresThisIteration)
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)!",
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)
failuresThisIteration
)
continue
}
@ -219,7 +227,8 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
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
}
@ -289,7 +298,9 @@ 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())
else JOB_ID_UPDATE_BACKGROUND,
componentName()
)
}
private fun Context.periodicBackgroundJobInfo(
@ -302,14 +313,17 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
.setPersisted(true)
.setRequiredNetworkType(
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY)
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)
setEstimatedNetworkBytes(
15000L * UPDATES_PER_ITERATION,
1000L * UPDATES_PER_ITERATION
)
}
}
.setRequiresCharging(requireCharging)

View File

@ -18,7 +18,8 @@ class FavoritesIntroDialog {
.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()
}

View File

@ -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,11 +180,12 @@ 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>) {
val localCategories = db.getCategories().executeAsBlocking()
@ -217,21 +225,24 @@ 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()
.post(
FormBody.Builder()
.add("favcat", gallery.category.toString())
.add("favnote", "")
.add("apply", "Add to Favorites")
.add("update", "1")
.build())
.build()
)
.build()
if (!explicitlyRetryExhRequest(10, request)) {
@ -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()}",
val result = galleryAdder.addGallery(
"${exh.baseUrl}${it.getUrl()}",
true,
exh,
throttleManager::throttle)
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
}
}
@ -404,9 +429,12 @@ sealed class FavoritesSyncStatus(val message: String) {
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)
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)
} else {
message
}
)
class CompleteWithErrors(messages: List<String>) : FavoritesSyncStatus(messages.joinToString("\n"))
}

View File

@ -21,7 +21,8 @@ class LocalFavoritesStorage {
fun getRealm() = Realm.getInstance(realmConfig)
fun getChangedDbEntries(realm: Realm) =
getChangedEntries(realm,
getChangedEntries(
realm,
parseToFavoriteEntries(
loadDbCategories(
db.getFavoriteMangas()
@ -32,12 +33,16 @@ class LocalFavoritesStorage {
)
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
getChangedEntries(realm,
getChangedEntries(
realm,
parseToFavoriteEntries(
entries.asSequence().map {
Pair(it.fav, it.manga.apply {
Pair(
it.fav,
it.manga.apply {
favorite = true
})
}
)
}
)
)
@ -100,8 +105,13 @@ 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
)
}
}
@ -115,10 +125,11 @@ class LocalFavoritesStorage {
token = EHentaiSearchMetadata.galleryToken(it.second.url)
category = it.first
if (this.category > MAX_CATEGORIES)
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)

View File

@ -71,13 +71,15 @@ 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()
@ -86,8 +88,9 @@ class HitomiNozomi(
}
.onErrorReturn { ByteArray(0) }
.map { inbuf ->
if (inbuf.isEmpty())
if (inbuf.isEmpty()) {
return@map emptyList<Int>()
}
val view = ByteCursor(inbuf)
val numberOfGalleryIds = view.nextInt()
@ -96,7 +99,8 @@ class HitomiNozomi(
if (numberOfGalleryIds > 10000000 ||
numberOfGalleryIds <= 0 ||
inbuf.size != expectedLength) {
inbuf.size != expectedLength
) {
return@map emptyList<Int>()
}
@ -112,11 +116,12 @@ 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
}
@ -203,9 +208,11 @@ 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())
.build()
)
.asObservableSuccess()
.map { resp ->
val body = resp.body!!.bytes()
@ -233,9 +240,12 @@ 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> {

View File

@ -50,7 +50,8 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
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>

View File

@ -35,10 +35,11 @@ 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) }

View File

@ -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)
@ -119,10 +120,11 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
private fun splitGalleryUrl(url: String) =
url.let {
// Only parse URL if is full URL
val pathSegments = if (it.startsWith("http"))
val pathSegments = if (it.startsWith("http")) {
Uri.parse(it).pathSegments
else
} else {
it.split('/')
}
pathSegments.filterNot(String::isNullOrBlank)
}

View File

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

View File

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

View File

@ -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"
}
}
val detailsDesc = StringBuilder()
artist?.let {

View File

@ -10,19 +10,23 @@ import exh.metadata.sql.tables.SearchTagTable
interface SearchTagQueries : DbProvider {
fun getSearchTagsForManga(mangaId: Long) = db.get()
.listOfObjects(SearchTag::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(SearchTagTable.TABLE)
.where("${SearchTagTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build())
.build()
)
.prepare()
fun deleteSearchTagsForManga(mangaId: Long) = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(SearchTagTable.TABLE)
.where("${SearchTagTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build())
.build()
)
.prepare()
fun insertSearchTag(searchTag: SearchTag) = db.put().`object`(searchTag).prepare()
@ -31,9 +35,11 @@ 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())
.build()
)
.prepare()
fun setSearchTagsForManga(mangaId: Long, tags: List<SearchTag>) {

View File

@ -10,19 +10,23 @@ import exh.metadata.sql.tables.SearchTitleTable
interface SearchTitleQueries : DbProvider {
fun getSearchTitlesForManga(mangaId: Long) = db.get()
.listOfObjects(SearchTitle::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(SearchTitleTable.TABLE)
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build())
.build()
)
.prepare()
fun deleteSearchTitlesForManga(mangaId: Long) = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(SearchTitleTable.TABLE)
.where("${SearchTitleTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build())
.build()
)
.prepare()
fun insertSearchTitle(searchTitle: SearchTitle) = db.put().`object`(searchTitle).prepare()
@ -31,9 +35,11 @@ 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())
.build()
)
.prepare()
fun setSearchTitlesForManga(mangaId: Long, titles: List<SearchTitle>) {

View File

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

View File

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

View File

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

View File

@ -9,7 +9,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
private val HIDE_SCRIPT = """
private val HIDE_SCRIPT =
"""
document.querySelector("#forgot_button").style.visibility = "hidden";
document.querySelector("#signup_button").style.visibility = "hidden";
document.querySelector("#announcement").style.visibility = "hidden";

View File

@ -16,8 +16,10 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
}
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
return ((EH_INTERCEPTORS[sourceId] ?: emptyList()) +
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())).merge()
return (
(EH_INTERCEPTORS[sourceId] ?: emptyList()) +
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())
).merge()
}
fun List<EHInterceptor>.merge(): EHInterceptor {

View File

@ -12,11 +12,12 @@ class SearchEngine {
component: Text?
): Pair<String, List<String>>? {
val maybeLenientComponent = component?.let {
if (!it.exact)
if (!it.exact) {
it.asLenientTagQueries()
else
} else {
listOf(it.asQuery())
}
}
val componentTagQuery = maybeLenientComponent?.let {
val params = mutableListOf<String>()
it.map { q ->
@ -25,7 +26,8 @@ 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 ?
@ -39,12 +41,14 @@ 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
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())
@ -86,16 +90,19 @@ class SearchEngine {
}
val completeParams = mutableListOf<String>()
var baseQuery = """
var baseQuery =
"""
SELECT ${SearchMetadataTable.COL_MANGA_ID}
FROM ${SearchMetadataTable.TABLE} meta
""".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
}

View File

@ -52,9 +52,9 @@ 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 })
rawText!!

View File

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

View File

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

View File

@ -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()
@ -37,6 +37,7 @@ class ConfiguringDialogController : DialogController() {
finish()
}
}
}
return MaterialDialog(activity!!)
.title(text = "Uploading settings to server")

View File

@ -30,14 +30,18 @@ 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())
.build()
)
.build()
)
.execute()
private val EHentai.uconfigUrl get() = baseUrl + UCONFIG_URL
@ -47,9 +51,11 @@ 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())
.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",
val response = source.execProfileActions(
"create",
PROFILE_NAME,
slot.toString(),
1)
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,13 +140,16 @@ 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 {
private const val PROFILE_NAME = "TachiyomiEH App"

View File

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

View File

@ -15,11 +15,14 @@ class WarnConfigureDialogController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(text = "Settings profile note")
.message(text = """
.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())
""".trimIndent()
)
.positiveButton(android.R.string.ok) {
prefs.eh_showSettingsUploadWarning().set(false)
ConfiguringDialogController().showDialog(router)
@ -29,10 +32,11 @@ class WarnConfigureDialogController : DialogController() {
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)
}
}
}
}

View File

@ -59,9 +59,11 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
.combineLatest(presenter.progressTotalRelay) { progress, total ->
// Show hide dismiss button
binding.progressDismissBtn.visibility =
if (progress == total)
if (progress == total) {
View.VISIBLE
else View.GONE
} else {
View.GONE
}
formatProgress(progress, total)
}.subscribeUntilDestroy {

View File

@ -40,10 +40,14 @@ class BatchAddPresenter : BasePresenter<BatchAddController>() {
failed.add(s)
}
progressRelay.call(i + 1)
eventRelay?.call((when (result) {
eventRelay?.call(
(
when (result) {
is GalleryAddEvent.Success -> "[OK]"
is GalleryAddEvent.Fail -> "[ERROR]"
}) + " " + result.logMessage)
}
) + " " + result.logMessage
)
}
// Show report

View File

@ -14,8 +14,9 @@ open class BasicWebViewClient(
if (verifyComplete(url)) {
activity.finish()
} else {
if (injectScript != null)
if (injectScript != null) {
view.evaluateJavascript("(function() {$injectScript})();", null)
}
}
}
}

View File

@ -69,9 +69,11 @@ class BrowserActionActivity : AppCompatActivity() {
}
} else null
val headers = ((source as? HttpSource)?.headers?.toMultimap()?.mapValues {
val headers = (
(source as? HttpSource)?.headers?.toMultimap()?.mapValues {
it.value.joinToString(",")
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
} ?: emptyMap()
) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
val cookies: HashMap<String, String>? =
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
@ -79,7 +81,8 @@ class BrowserActionActivity : AppCompatActivity() {
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()
credentialsObservable = httpClient.newCall(
Request.Builder()
// Rob demo credentials
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
.build())
.build()
)
.asObservableSuccess()
.subscribeOn(Schedulers.io())
.map {
@ -192,13 +197,19 @@ class BrowserActionActivity : AppCompatActivity() {
when (stage) {
STAGE_CHECKBOX -> {
if (result!!.toBoolean()) {
webview.postDelayed({
webview.postDelayed(
{
getAudioButtonLocation(loopId)
}, 250)
},
250
)
} else {
webview.postDelayed({
webview.postDelayed(
{
doStageCheckbox(loopId)
}, 250)
},
250
)
}
}
STAGE_GET_AUDIO_BTN_LOCATION -> {
@ -216,9 +227,12 @@ class BrowserActionActivity : AppCompatActivity() {
doStageDownloadAudio(loopId)
}
} else {
webview.postDelayed({
webview.postDelayed(
{
getAudioButtonLocation(loopId)
}, 250)
},
250
)
}
}
STAGE_DOWNLOAD_AUDIO -> {
@ -226,21 +240,30 @@ class BrowserActionActivity : AppCompatActivity() {
Timber.d("Got audio URL: $result")
performRecognize(result)
.observeOn(Schedulers.io())
.subscribe({
.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({
webview.postDelayed(
{
doStageDownloadAudio(loopId)
}, 250)
},
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",
.addFormDataPart(
"audio.mp3",
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile))
.build())
.build()).asObservableSuccess()
"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({
webview.postDelayed(
{
runValidateCaptcha(loopId)
}, 250)
},
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();
@ -525,7 +578,8 @@ class BrowserActionActivity : AppCompatActivity() {
}
""".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));
@ -540,7 +594,8 @@ class BrowserActionActivity : AppCompatActivity() {
alert("exh-");
""".trimIndent()
val SOLVE_UI_SCRIPT_SHOW = """
val SOLVE_UI_SCRIPT_SHOW =
"""
(function() {
let exh_overlay = document.createElement("div");
exh_overlay.id = "exh_overlay";
@ -570,7 +625,8 @@ class BrowserActionActivity : AppCompatActivity() {
})();
""".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");
@ -579,7 +635,8 @@ class BrowserActionActivity : AppCompatActivity() {
})();
""".trimIndent()
val RECOGNIZE_JSON = """
val RECOGNIZE_JSON =
"""
{
"part_content_type": "audio/mp3",
"keywords": [],
@ -689,7 +746,8 @@ class BrowserActionActivity : AppCompatActivity() {
}
}
class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source),
class NoopActionCompletionVerifier(private val source: HttpSource) :
DelegatedHttpSource(source),
ActionCompletionVerifier {
override val versionId get() = source.versionId
override val lang: String get() = source.lang

View File

@ -61,10 +61,12 @@ class InterceptActivity : BaseRxActivity<EhActivityInterceptBinding, InterceptAc
binding.interceptProgress.gone()
binding.interceptStatus.text = "Launching app..."
onBackPressed()
startActivity(Intent(this, MainActivity::class.java)
startActivity(
Intent(this, MainActivity::class.java)
.setAction(MainActivity.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(MangaController.MANGA_EXTRA, it.mangaId))
.putExtra(MangaController.MANGA_EXTRA, it.mangaId)
)
}
is InterceptResult.Failure -> {
binding.interceptProgress.gone()

View File

@ -21,12 +21,14 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
thread {
val result = galleryAdder.addGallery(gallery)
status.onNext(when (result) {
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)
})
}
)
}
}
}

View File

@ -44,39 +44,44 @@ 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)
fun tryChange() {
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)

View File

@ -20,8 +20,10 @@ 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) {

View File

@ -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,8 +94,9 @@ 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)

View File

@ -53,12 +53,15 @@ 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. " +
.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.")
"Press OK to grant this permission now."
)
.negativeButton(R.string.action_cancel)
.positiveButton(android.R.string.ok) {
try {
@ -67,8 +70,10 @@ fun notifyLockSecurity(
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'.")
.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)

View File

@ -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,8 +129,10 @@ 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.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)
) &&
it.value.isNotBlank()
}.count() >= 2
}
@ -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';

View File

@ -14,7 +14,7 @@ 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)
@ -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 {
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 = {
log += sec(
"[${values.joinToString(
separator = ", ",
transform = {
"\"$it\""
})}] IN \"$fieldName\"" + (casing?.let {
}
)}] 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 {
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 {
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 {
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 {
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 {
log += sec(
"\"$fieldName\" LIKE \"$value\"" + (
casing?.let {
" CASE ${casing.name}"
} ?: ""))
} ?: ""
)
)
}
fun like(fieldName: String, value: String): RealmQuery<E> {

View File

@ -209,10 +209,14 @@ class NakedTrie<T> : MutableMap<String, T> {
override val entries: Set<Map.Entry<String, T>>
get() {
val out = mutableSetOf<Map.Entry<String, T>>()
node.walk("", { k, v ->
node.walk(
"",
{ k, v ->
out.add(AbstractMap.SimpleImmutableEntry(k, v))
true
}, leavesOnly)
},
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, _ ->
node.walk(
"",
{ k, _ ->
out.add(k)
true
}, leavesOnly)
},
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 ->
node.walk(
"",
{ _, v ->
out.add(v)
true
}, leavesOnly)
},
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 ->
node.walk(
"",
{ _, v ->
if (v == value) return true
true
}, leavesOnly)
},
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 {
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 {
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 {
get() = FakeMutableCollection.fromCollection(
mutableListOf<T>().apply {
walk { _, v ->
this += v
true
}
})
}
)
}

View File

@ -9,7 +9,8 @@ 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))

View File

@ -37,14 +37,18 @@ suspend fun <T> Single<T>.await(subscribeOn: Scheduler? = null): T {
return suspendCancellableCoroutine { continuation ->
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
lateinit var sub: Subscription
sub = self.subscribe({
sub = self.subscribe(
{
continuation.resume(it) {
sub.unsubscribe()
}
}, {
if (!continuation.isCancelled)
},
{
if (!continuation.isCancelled) {
continuation.resumeWithException(it)
})
}
}
)
continuation.invokeOnCancellation {
sub.unsubscribe()
@ -59,14 +63,18 @@ suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) {
return suspendCancellableCoroutine { continuation ->
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
lateinit var sub: Subscription
sub = self.subscribe({
sub = self.subscribe(
{
continuation.resume(Unit) {
sub.unsubscribe()
}
}, {
if (!continuation.isCancelled)
},
{
if (!continuation.isCancelled) {
continuation.resumeWithException(it)
})
}
}
)
continuation.invokeOnCancellation {
sub.unsubscribe()

View File

@ -18,10 +18,16 @@ fun UrlImportableSource.urlImportFetchSearchManga(query: String, fail: () -> Obs
query.startsWith("http://") || query.startsWith("https://") -> {
Observable.fromCallable {
val res = galleryAdder.addGallery(query, false, this)
MangasPage((if (res is GalleryAddEvent.Success)
MangasPage(
(
if (res is GalleryAddEvent.Success) {
listOf(res.manga)
else
emptyList()), false)
} else {
emptyList()
}
),
false
)
}
}
else -> fail()

View File

@ -54,31 +54,35 @@ class ByteCursor(val content: ByteArray) {
}
fun expect(vararg bytes: Byte) {
if (bytes.size > remaining())
if (bytes.size > remaining()) {
throw IllegalStateException("Unexpected end of content!")
}
for (i in 0..bytes.lastIndex) {
val expected = bytes[i]
val actual = content[index + i + 1]
if (expected != actual)
if (expected != actual) {
throw IllegalStateException("Unexpected content (expected: $expected, actual: $actual)!")
}
}
index += bytes.size
}
fun checkEqual(vararg bytes: Byte): Boolean {
if (bytes.size > remaining())
if (bytes.size > remaining()) {
return false
}
for (i in 0..bytes.lastIndex) {
val expected = bytes[i]
val actual = content[index + i + 1]
if (expected != actual)
if (expected != actual) {
return false
}
}
return true
}

View File

@ -147,10 +147,12 @@ class GroupSerializer(override val serializer: FilterSerializer) : Serializer<Fi
override fun serialize(json: JsonObject, filter: Filter.Group<Any?>) {
json[STATE] = JsonArray().apply {
filter.state.forEach {
add(if (it is Filter<*>)
add(
if (it is Filter<*>) {
serializer.serialize(it as Filter<Any?>)
else
} else {
JsonNull.INSTANCE
}
)
}
}
@ -158,10 +160,11 @@ class GroupSerializer(override val serializer: FilterSerializer) : Serializer<Fi
override fun deserialize(json: JsonObject, filter: Filter.Group<Any?>) {
json[STATE].asJsonArray.forEachIndexed { index, jsonElement ->
if (!jsonElement.isJsonNull)
if (!jsonElement.isJsonNull) {
serializer.deserialize(filter.state[index] as Filter<Any?>, jsonElement.asJsonObject)
}
}
}
override fun mappings() = listOf(
Pair(NAME, Filter.Group<Any?>::name)
@ -195,8 +198,10 @@ class SortSerializer(override val serializer: FilterSerializer) : Serializer<Fil
override fun deserialize(json: JsonObject, filter: Filter.Sort) {
// Deserialize state
filter.state = json[STATE].nullObj?.let {
Filter.Sort.Selection(it[STATE_INDEX].int,
it[STATE_ASCENDING].bool)
Filter.Sort.Selection(
it[STATE_INDEX].int,
it[STATE_ASCENDING].bool
)
}
}

View File

@ -44,6 +44,7 @@ buildscript {
repositories {
google()
jcenter()
maven { setUrl("https://maven.fabric.io/public") }
}
}