Optimize imports, disallow wildcard imports because of klint, run linter

This commit is contained in:
jobobby04 2020-04-04 16:30:05 -04:00 committed by Jobobby04
parent f18891a07e
commit 23ac3d18e5
138 changed files with 1192 additions and 1027 deletions

View File

@ -43,7 +43,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
put(COL_FLAGS, obj.flags) put(COL_FLAGS, obj.flags)
val orderString = obj.mangaOrder.joinToString("/") val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString) put(COL_MANGA_ORDER, orderString)
} }
} }

View File

@ -29,6 +29,4 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_URL, manga.url) put(MangaTable.COL_URL, manga.url)
} }
} }

View File

@ -24,7 +24,6 @@ object CategoryTable {
$COL_MANGA_ORDER TEXT NOT NULL $COL_MANGA_ORDER TEXT NOT NULL
)""" )"""
val addMangaOrder: String val addMangaOrder: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
} }

View File

@ -72,4 +72,4 @@ class LibraryUpdateNotifier(private val context: Context) {
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

View File

@ -45,4 +45,4 @@ class EmptyPreferenceDataStore : PreferenceDataStore() {
override fun putStringSet(key: String?, values: Set<String>?) { override fun putStringSet(key: String?, values: Set<String>?) {
} }
} }

View File

@ -57,4 +57,4 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
return newBody.toString().toRequestBody(requestBody.contentType()) return newBody.toString().toRequestBody(requestBody.contentType())
} }
} }

View File

@ -110,4 +110,4 @@ internal class UpdaterNotifier(private val context: Context) {
} }
notificationBuilder.show(Notifications.ID_UPDATER) notificationBuilder.show(Notifications.ID_UPDATER)
} }
} }

View File

@ -5,4 +5,4 @@ import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source { interface ConfigurableSource : Source {
fun setupPreferenceScreen(screen: PreferenceScreen) fun setupPreferenceScreen(screen: PreferenceScreen)
} }

View File

@ -9,11 +9,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import kotlin.reflect.KClass
import rx.Completable import rx.Completable
import rx.Single import rx.Single
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.reflect.KClass
/** /**
* LEWD! * LEWD!

View File

@ -28,4 +28,4 @@ interface UrlImportableSource : Source {
url url
} }
} }
} }

View File

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.* import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.set
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
@ -13,7 +18,12 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.asObservableWithAsyncStacktrace import eu.kanade.tachiyomi.network.asObservableWithAsyncStacktrace
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -36,23 +46,30 @@ import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.ignore import exh.util.ignore
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder
import java.util.ArrayList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.* import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
import rx.Single import rx.Single
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.util.*
// TODO Consider gallery updating when doing tabbed browsing // TODO Consider gallery updating when doing tabbed browsing
class EHentai(override val id: Long, class EHentai(
val exh: Boolean, override val id: Long,
val context: Context) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource { val exh: Boolean,
val context: Context
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
override val metaClass = EHentaiSearchMetadata::class override val metaClass = EHentaiSearchMetadata::class
val schema: String val schema: String
@ -98,10 +115,10 @@ class EHentai(override val id: Long,
favElement?.attr("style")?.substring(14, 17) favElement?.attr("style")?.substring(14, 17)
), ),
manga = Manga.create(id).apply { manga = Manga.create(id).apply {
//Get title // Get title
title = thumbnailElement.attr("title") title = thumbnailElement.attr("title")
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href")) url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
//Get image // Get image
thumbnail_url = thumbnailElement.attr("src") thumbnail_url = thumbnailElement.attr("src")
// TODO Parse genre + uploader + tags // TODO Parse genre + uploader + tags
@ -110,9 +127,9 @@ class EHentai(override val id: Long,
val parsedLocation = doc.location().toHttpUrlOrNull() val parsedLocation = doc.location().toHttpUrlOrNull()
//Add to page if required // Add to page if required
val hasNextPage = if (parsedLocation == null val hasNextPage = if (parsedLocation == null ||
|| !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) { !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
select("a[onclick=return false]").last()?.let { select("a[onclick=return false]").last()?.let {
it.text() == ">" it.text() == ">"
} ?: false } ?: false
@ -212,8 +229,11 @@ class EHentai(override val id: Long,
} }
}!! }!!
private fun fetchChapterPage(chapter: SChapter, np: String, private fun fetchChapterPage(
pastUrls: List<String> = emptyList()): Observable<List<String>> { chapter: SChapter,
np: String,
pastUrls: List<String> = emptyList()
): Observable<List<String>> {
val urls = ArrayList(pastUrls) val urls = ArrayList(pastUrls)
return chapterPageCall(np).flatMap { return chapterPageCall(np).flatMap {
val jsoup = it.asJsoup() val jsoup = it.asJsoup()
@ -245,7 +265,7 @@ class EHentai(override val id: Long,
else else
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
searchMangaRequestObservable(page, query, filters).flatMap { searchMangaRequestObservable(page, query, filters).flatMap {
@ -377,7 +397,7 @@ class EHentai(override val id: Long,
uploader = select("#gdn").text().nullIfBlank()?.trim() uploader = select("#gdn").text().nullIfBlank()?.trim()
//Parse the table // Parse the table
select("#gdd tr").forEach { select("#gdd tr").forEach {
val left = it.select(".gdt1").text().nullIfBlank()?.trim() val left = it.select(".gdt1").text().nullIfBlank()?.trim()
val rightElement = it.selectFirst(".gdt2") val rightElement = it.selectFirst(".gdt2")
@ -407,13 +427,13 @@ class EHentai(override val id: Long,
} }
lastUpdateCheck = System.currentTimeMillis() lastUpdateCheck = System.currentTimeMillis()
if (datePosted != null if (datePosted != null &&
&& lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) { lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
aged = true aged = true
XLog.d("aged %s - too old", title) XLog.d("aged %s - too old", title)
} }
//Parse ratings // Parse ratings
ignore { ignore {
averageRating = select("#rating_label") averageRating = select("#rating_label")
.text() .text()
@ -428,7 +448,7 @@ class EHentai(override val id: Long,
?.toInt() ?.toInt()
} }
//Parse tags // Parse tags
tags.clear() tags.clear()
select("#taglist tr").forEach { select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":") val namespace = it.select(".tc").text().removeSuffix(":")
@ -465,7 +485,7 @@ class EHentai(override val id: Long,
fun realImageUrlParse(response: Response, page: Page): String { fun realImageUrlParse(response: Response, page: Page): String {
with(response.asJsoup()) { with(response.asJsoup()) {
val currentImage = getElementById("img").attr("src") val currentImage = getElementById("img").attr("src")
//Each press of the retry button will choose another server // Each press of the retry button will choose another server
select("#loadfail").attr("onclick").nullIfBlank()?.let { select("#loadfail").attr("onclick").nullIfBlank()?.let {
page.url = addParam(page.url, "nl", it.substring(it.indexOf('\'') + 1 until it.lastIndexOf('\''))) page.url = addParam(page.url, "nl", it.substring(it.indexOf('\'') + 1 until it.lastIndexOf('\'')))
} }
@ -490,17 +510,17 @@ class EHentai(override val id: Long,
cache = false)).execute() cache = false)).execute()
val doc = response2.asJsoup() val doc = response2.asJsoup()
//Parse favorites // Parse favorites
val parsed = extendedGenericMangaParse(doc) val parsed = extendedGenericMangaParse(doc)
result += parsed.first result += parsed.first
//Parse fav names // Parse fav names
if (favNames == null) if (favNames == null)
favNames = doc.select(".fp:not(.fps)").mapNotNull { favNames = doc.select(".fp:not(.fps)").mapNotNull {
it.child(2).text() it.child(2).text()
} }
//Next page // Next page
page++ page++
} while (parsed.second) } while (parsed.second)
@ -544,7 +564,7 @@ class EHentai(override val id: Long,
fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp)) fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp))
//Headers // Headers
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!! override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
fun addParam(url: String, param: String, value: String) = Uri.parse(url) fun addParam(url: String, param: String, value: String) = Uri.parse(url)
@ -565,7 +585,7 @@ class EHentai(override val id: Long,
chain.proceed(newReq) chain.proceed(newReq)
}.build()!! }.build()!!
//Filters // Filters
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Watched(), Watched(),
GenreGroup(), GenreGroup(),
@ -673,11 +693,11 @@ class EHentai(override val id: Long,
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
return when (uri.pathSegments.firstOrNull()) { return when (uri.pathSegments.firstOrNull()) {
"g" -> { "g" -> {
//Is already gallery page, do nothing // Is already gallery page, do nothing
uri.toString() uri.toString()
} }
"s" -> { "s" -> {
//Is page, fetch gallery token and use that // Is page, fetch gallery token and use that
getGalleryUrlFromPage(uri) getGalleryUrlFromPage(uri)
} }
else -> null else -> null
@ -713,7 +733,6 @@ class EHentai(override val id: Long,
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/" return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
} }
companion object { companion object {
private const val QUERY_PREFIX = "?f_apply=Apply+Filter" private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
private const val TR_SUFFIX = "TR" private const val TR_SUFFIX = "TR"
@ -738,6 +757,5 @@ class EHentai(override val id: Long,
fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") { fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}" "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
} }
} }
} }

View File

@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -24,6 +28,8 @@ import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -32,8 +38,6 @@ import rx.Observable
import rx.Single import rx.Single
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.*
/** /**
* Man, I hate this source :( * Man, I hate this source :(
@ -61,8 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private var tagIndexVersionCacheTime: Long = 0 private var tagIndexVersionCacheTime: Long = 0
private fun tagIndexVersion(): Single<Long> { private fun tagIndexVersion(): Single<Long> {
val sCachedTagIndexVersion = cachedTagIndexVersion val sCachedTagIndexVersion = cachedTagIndexVersion
return if (sCachedTagIndexVersion == null 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 { HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis() tagIndexVersionCacheTime = System.currentTimeMillis()
@ -76,8 +80,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private var galleryIndexVersionCacheTime: Long = 0 private var galleryIndexVersionCacheTime: Long = 0
private fun galleryIndexVersion(): Single<Long> { private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null 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 { HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it cachedGalleryIndexVersion = it
galleryIndexVersionCacheTime = System.currentTimeMillis() galleryIndexVersionCacheTime = System.currentTimeMillis()
@ -307,7 +311,6 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
} }
} }
/** /**
* Returns an observable with the updated details for a manga. Normally it's not needed to * Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method. * override this method.
@ -423,5 +426,4 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US) SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
} }
} }
} }

View File

@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.* import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.nullArray
import com.github.salomonbrys.kotson.nullLong
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -30,8 +39,8 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
override val metaClass = NHentaiSearchMetadata::class override val metaClass = NHentaiSearchMetadata::class
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
//TODO There is currently no way to get the most popular mangas // TODO There is currently no way to get the most popular mangas
//TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen // TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
return fetchLatestUpdates(page) return fetchLatestUpdates(page)
} }
@ -39,7 +48,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val trimmedIdQuery = query.trim().removePrefix("id:") val trimmedIdQuery = query.trim().removePrefix("id:")
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) { val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
@ -246,7 +255,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
override fun getFilterList() = FilterList(SortFilter(), filterLang()) override fun getFilterList() = FilterList(SortFilter(), filterLang())
//language filtering // language filtering
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray()) private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
class SortFilter : Filter.Sort( class SortFilter : Filter.Sort(
@ -305,7 +314,6 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
Pair("Chinese", " chinese") Pair("Chinese", " chinese")
) )
val jsonParser by lazy { val jsonParser by lazy {
JsonParser() JsonParser()
} }

View File

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -17,14 +22,15 @@ import exh.metadata.metadata.base.RaisedTag
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
import java.text.SimpleDateFormat
import java.util.*
// TODO Transform into delegated source // TODO Transform into delegated source
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(), class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(),
@ -54,7 +60,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
override fun popularMangaNextPageSelector(): String? = null override fun popularMangaNextPageSelector(): String? = null
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
@ -256,7 +262,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
} }
} }
//Explicit type arg for listOf() to workaround this: KT-16570 // 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(), ReleaseYearRangeFilter(),
ReleaseYearYearFilter() ReleaseYearYearFilter()

View File

@ -2,10 +2,14 @@ package eu.kanade.tachiyomi.source.online.english
import android.net.Uri import android.net.Uri
import com.kizitonwose.time.hours import com.kizitonwose.time.hours
import hu.akarnokd.rxjava.interop.RxJavaInterop
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -17,13 +21,18 @@ import exh.util.CachedField
import exh.util.NakedTrie import exh.util.NakedTrie
import exh.util.await import exh.util.await
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.* import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element

View File

@ -4,12 +4,16 @@ import android.net.Uri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.string import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser import com.google.gson.JsonParser
import hu.akarnokd.rxjava.interop.RxJavaInterop
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -23,7 +27,9 @@ import exh.search.Text
import exh.util.await import exh.util.await
import exh.util.dropBlank import exh.util.dropBlank
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import info.debatty.java.stringsimilarity.Levenshtein import info.debatty.java.stringsimilarity.Levenshtein
import kotlin.math.ceil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -36,7 +42,6 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import kotlin.math.ceil
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource { class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
/** /**
@ -182,7 +187,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
} }
} }
"/result" "/result"
} else { } else {
"/search" "/search"

View File

@ -30,7 +30,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
*/ */
override val metaClass = HentaiCafeSearchMetadata::class override val metaClass = HentaiCafeSearchMetadata::class
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)

View File

@ -29,7 +29,7 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
*/ */
override val metaClass = PururinSearchMetadata::class override val metaClass = PururinSearchMetadata::class
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val trimmedIdQuery = query.trim().removePrefix("id:") val trimmedIdQuery = query.trim().removePrefix("id:")
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) { val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
@ -106,4 +106,4 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}" return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
} }
} }

View File

@ -4,32 +4,28 @@ import android.net.Uri
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.system.asJsoup import eu.kanade.tachiyomi.util.system.asJsoup
import exh.metadata.metadata.TsuminoSearchMetadata import exh.metadata.metadata.TsuminoSearchMetadata
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
import java.text.SimpleDateFormat
import java.util.*
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate), class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource { LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class; override val metaClass = TsuminoSearchMetadata::class
override val lang = "en" override val lang = "en"
//Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)

View File

@ -7,4 +7,4 @@ interface TabbedController {
fun configureTabs(tabs: TabLayout) {} fun configureTabs(tabs: TabLayout) {}
fun cleanupTabs(tabs: TabLayout) {} fun cleanupTabs(tabs: TabLayout) {}
} }

View File

@ -8,4 +8,4 @@ abstract class BaseViewHolder(view: View) : androidx.recyclerview.widget.Recycle
override val containerView: View? override val containerView: View?
get() = itemView get() = itemView
} }

View File

@ -42,4 +42,4 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val check: CheckBox = itemView.findViewById(R.id.nav_view_item) val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -54,4 +54,4 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
val text: TextView = itemView.findViewById(R.id.nav_view_item_text) val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item) val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.source.filter package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible

View File

@ -5,21 +5,24 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import exh.isLewdSource import exh.isLewdSource
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
import exh.search.SearchEngine import exh.search.SearchEngine
import exh.util.await import exh.util.await
import exh.util.cancellable import exh.util.cancellable
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
/** /**
* Adapter storing a list of manga in a certain category. * Adapter storing a list of manga in a certain category.

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
@ -26,7 +26,6 @@ abstract class LibraryHolder(
*/ */
abstract fun onSetValues(item: LibraryItem) abstract fun onSetValues(item: LibraryItem)
/** /**
* Called when an item is released. * Called when an item is released.
* *
@ -36,5 +35,4 @@ abstract class LibraryHolder(
super.onItemReleased(position) super.onItemReleased(position)
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position) (adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
} }
} }

View File

@ -235,4 +235,3 @@ class MangaInfoPresenter(
return toInsert return toInsert
} }
} }

View File

@ -46,4 +46,4 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holde
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
} }
} }
} }

View File

@ -17,7 +17,17 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.preference.* import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
@ -34,6 +44,7 @@ import exh.ui.login.LoginController
import exh.util.await import exh.util.await
import exh.util.trans import exh.util.trans
import humanize.Humanize import humanize.Humanize
import java.util.Date
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,7 +52,6 @@ import kotlinx.coroutines.withContext
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.*
/** /**
* EH Settings fragment * EH Settings fragment
@ -52,17 +62,17 @@ class SettingsEhController : SettingsController() {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private fun Preference<*>.reconfigure(): Boolean { private fun Preference<*>.reconfigure(): Boolean {
//Listen for change commit // Listen for change commit
asObservable() asObservable()
.skip(1) //Skip first as it is emitted immediately .skip(1) // Skip first as it is emitted immediately
.take(1) //Only listen for first commit .take(1) // Only listen for first commit
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
//Only listen for first change commit // Only listen for first change commit
WarnConfigureDialogController.uploadSettings(router) WarnConfigureDialogController.uploadSettings(router)
} }
//Always return true to save changes // Always return true to save changes
return true return true
} }

View File

@ -23,4 +23,4 @@ fun Element.attrOrText(css: String): String {
*/ */
fun Response.asJsoup(html: String? = null): Document { fun Response.asJsoup(html: String? = null): Document {
return Jsoup.parse(html ?: body!!.string(), request.url.toString()) return Jsoup.parse(html ?: body!!.string(), request.url.toString())
} }

View File

@ -16,4 +16,4 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, tint: Int? = null) {
vector?.setTint(tint) vector?.setTint(tint)
} }
setImageDrawable(vector) setImageDrawable(vector)
} }

View File

@ -12,4 +12,4 @@ import androidx.annotation.LayoutRes
*/ */
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View { fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layout, this, attachToRoot) return LayoutInflater.from(context).inflate(layout, this, attachToRoot)
} }

View File

@ -23,4 +23,4 @@ class IntListPreference @JvmOverloads constructor(context: Context, attrs: Attri
defaultReturnValue defaultReturnValue
} }
} }
} }

View File

@ -53,7 +53,7 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
}.map { it.value.sourceId }.sorted() }.map { it.value.sourceId }.sorted()
// This method MUST be fast! // This method MUST be fast!
fun isLewdSource(source: Long) = source in 6900..6999 fun isLewdSource(source: Long) = source in 6900..6999 ||
|| lewdDelegatedSourceIds.binarySearch(source) >= 0 lewdDelegatedSourceIds.binarySearch(source) >= 0
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID

View File

@ -2,6 +2,8 @@ package exh
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
data class EXHSavedSearch(val name: String, data class EXHSavedSearch(
val query: String, val name: String,
val filterList: FilterList) val query: String,
val filterList: FilterList
)

View File

@ -16,20 +16,22 @@ class GalleryAdder {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
fun addGallery(url: String, fun addGallery(
fav: Boolean = false, url: String,
forceSource: UrlImportableSource? = null, fav: Boolean = false,
throttleFunc: () -> Unit = {}): GalleryAddEvent { forceSource: UrlImportableSource? = null,
throttleFunc: () -> Unit = {}
): GalleryAddEvent {
XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource) XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource)
try { try {
val uri = Uri.parse(url) val uri = Uri.parse(url)
// Find matching source // Find matching source
val source = if(forceSource != null) { val source = if (forceSource != null) {
try { try {
if (forceSource.matchesUri(uri)) forceSource if (forceSource.matchesUri(uri)) forceSource
else return GalleryAddEvent.Fail.UnknownType(url) else return GalleryAddEvent.Fail.UnknownType(url)
} catch(e: Exception) { } catch (e: Exception) {
XLog.e("Source URI match check error!", e) XLog.e("Source URI match check error!", e)
return GalleryAddEvent.Fail.UnknownType(url) return GalleryAddEvent.Fail.UnknownType(url)
} }
@ -39,7 +41,7 @@ class GalleryAdder {
.find { .find {
try { try {
it.matchesUri(uri) it.matchesUri(uri)
} catch(e: Exception) { } catch (e: Exception) {
XLog.e("Source URI match check error!", e) XLog.e("Source URI match check error!", e)
false false
} }
@ -49,7 +51,7 @@ class GalleryAdder {
// Map URL to manga URL // Map URL to manga URL
val realUrl = try { val realUrl = try {
source.mapUrlToMangaUrl(uri) source.mapUrlToMangaUrl(uri)
} catch(e: Exception) { } catch (e: Exception) {
XLog.e("Source URI map-to-manga error!", e) XLog.e("Source URI map-to-manga error!", e)
null null
} ?: return GalleryAddEvent.Fail.UnknownType(url) } ?: return GalleryAddEvent.Fail.UnknownType(url)
@ -57,12 +59,12 @@ class GalleryAdder {
// Clean URL // Clean URL
val cleanedUrl = try { val cleanedUrl = try {
source.cleanMangaUrl(realUrl) source.cleanMangaUrl(realUrl)
} catch(e: Exception) { } catch (e: Exception) {
XLog.e("Source URI clean error!", e) XLog.e("Source URI clean error!", e)
null null
} ?: return GalleryAddEvent.Fail.UnknownType(url) } ?: return GalleryAddEvent.Fail.UnknownType(url)
//Use manga in DB if possible, otherwise, make a new manga // Use manga in DB if possible, otherwise, make a new manga
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking() val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
?: Manga.create(source.id).apply { ?: Manga.create(source.id).apply {
this.url = cleanedUrl this.url = cleanedUrl
@ -71,7 +73,7 @@ class GalleryAdder {
// Insert created manga if not in DB before fetching details // Insert created manga if not in DB before fetching details
// This allows us to keep the metadata when fetching details // This allows us to keep the metadata when fetching details
if(manga.id == null) { if (manga.id == null) {
db.insertManga(manga).executeAsBlocking().insertedId()?.let { db.insertManga(manga).executeAsBlocking().insertedId()?.let {
manga.id = it manga.id = it
} }
@ -86,9 +88,9 @@ class GalleryAdder {
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
//Fetch and copy chapters // Fetch and copy chapters
try { try {
val chapterListObs = if(source is EHentai) { val chapterListObs = if (source is EHentai) {
source.fetchChapterList(manga, throttleFunc) source.fetchChapterList(manga, throttleFunc)
} else { } else {
source.fetchChapterList(manga) source.fetchChapterList(manga)
@ -102,10 +104,10 @@ class GalleryAdder {
} }
return GalleryAddEvent.Success(url, manga) return GalleryAddEvent.Success(url, manga)
} catch(e: Exception) { } catch (e: Exception) {
XLog.w("Could not add gallery (url: $url)!", e) XLog.w("Could not add gallery (url: $url)!", e)
if(e is EHentai.GalleryNotFoundException) { if (e is EHentai.GalleryNotFoundException) {
return GalleryAddEvent.Fail.NotFound(url) return GalleryAddEvent.Fail.NotFound(url)
} }
@ -120,21 +122,25 @@ sealed class GalleryAddEvent {
abstract val galleryUrl: String abstract val galleryUrl: String
open val galleryTitle: String? = null open val galleryTitle: String? = null
class Success(override val galleryUrl: String, class Success(
val manga: Manga): GalleryAddEvent() { override val galleryUrl: String,
val manga: Manga
) : GalleryAddEvent() {
override val galleryTitle = manga.title override val galleryTitle = manga.title
override val logMessage = "Added gallery: $galleryTitle" override val logMessage = "Added gallery: $galleryTitle"
} }
sealed class Fail: GalleryAddEvent() { sealed class Fail : GalleryAddEvent() {
class UnknownType(override val galleryUrl: String): Fail() { class UnknownType(override val galleryUrl: String) : Fail() {
override val logMessage = "Unknown gallery type for gallery: $galleryUrl" override val logMessage = "Unknown gallery type for gallery: $galleryUrl"
} }
open class Error(override val galleryUrl: String, open class Error(
override val logMessage: String): Fail() override val galleryUrl: String,
override val logMessage: String
) : Fail()
class NotFound(galleryUrl: String): class NotFound(galleryUrl: String) :
Error(galleryUrl, "Gallery does not exist: $galleryUrl") Error(galleryUrl, "Gallery does not exist: $galleryUrl")
} }
} }

View File

@ -9,20 +9,19 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.jobScheduler import eu.kanade.tachiyomi.util.system.jobScheduler
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.EXHMigrations import exh.EXHMigrations
import exh.EXH_SOURCE_ID
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.await import exh.util.await
import exh.util.cancellable import exh.util.cancellable
import uy.kohesive.injekt.injectLazy
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
object DebugFunctions { object DebugFunctions {
val app: Application by injectLazy() val app: Application by injectLazy()
@ -31,7 +30,7 @@ object DebugFunctions {
val sourceManager: SourceManager by injectLazy() val sourceManager: SourceManager by injectLazy()
fun forceUpgradeMigration() { fun forceUpgradeMigration() {
prefs.eh_lastVersionCode().set(0) prefs.eh_lastVersionCode().set(0)
EXHMigrations.upgrade(prefs) EXHMigrations.upgrade(prefs)
} }
@ -47,7 +46,7 @@ object DebugFunctions {
for (manga in allManga) { for (manga in allManga) {
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>() val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
if(meta != null) { if (meta != null) {
// remove age flag // remove age flag
meta.aged = false meta.aged = false
db.insertFlatMetadata(meta.flatten()).await() db.insertFlatMetadata(meta.flatten()).await()

View File

@ -26,4 +26,4 @@ enum class DebugToggles(val default: Boolean) {
companion object { companion object {
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
} }
} }

View File

@ -8,8 +8,12 @@ import android.widget.HorizontalScrollView
import android.widget.TextView import android.widget.TextView
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.ui.setting.* import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.* import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference
import kotlin.reflect.KVisibility import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredFunctions import kotlin.reflect.full.declaredFunctions
@ -41,7 +45,7 @@ class SettingsDebugController : SettingsController() {
view.text = "Function returned result:\n\n$result" view.text = "Function returned result:\n\n$result"
MaterialDialog.Builder(context) MaterialDialog.Builder(context)
.customView(hView, true) .customView(hView, true)
} catch(t: Throwable) { } catch (t: Throwable) {
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}" view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
MaterialDialog.Builder(context) MaterialDialog.Builder(context)
.customView(hView, true) .customView(hView, true)
@ -59,8 +63,8 @@ class SettingsDebugController : SettingsController() {
title = it.name.replace('_', ' ').toLowerCase().capitalize() title = it.name.replace('_', ' ').toLowerCase().capitalize()
key = it.prefKey key = it.prefKey
defaultValue = it.default defaultValue = it.default
summaryOn = if(it.default) "" else MODIFIED_TEXT summaryOn = if (it.default) "" else MODIFIED_TEXT
summaryOff = if(it.default) MODIFIED_TEXT else "" summaryOff = if (it.default) MODIFIED_TEXT else ""
} }
} }
} }
@ -74,4 +78,4 @@ class SettingsDebugController : SettingsController() {
companion object { companion object {
private val MODIFIED_TEXT = Html.fromHtml("<font color='red'>MODIFIED</font>") private val MODIFIED_TEXT = Html.fromHtml("<font color='red'>MODIFIED</font>")
} }
} }

View File

@ -1,19 +1,21 @@
package exh.eh package exh.eh
class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX, class EHentaiThrottleManager(
private val inc: Int = THROTTLE_INC) { private val max: Int = THROTTLE_MAX,
private val inc: Int = THROTTLE_INC
) {
private var lastThrottleTime: Long = 0 private var lastThrottleTime: Long = 0
var throttleTime: Long = 0 var throttleTime: Long = 0
private set private set
fun throttle() { fun throttle() {
//Throttle requests if necessary // Throttle requests if necessary
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val timeDiff = now - lastThrottleTime val timeDiff = now - lastThrottleTime
if(timeDiff < throttleTime) if (timeDiff < throttleTime)
Thread.sleep(throttleTime - timeDiff) Thread.sleep(throttleTime - timeDiff)
if(throttleTime < max) if (throttleTime < max)
throttleTime += inc throttleTime += inc
lastThrottleTime = System.currentTimeMillis() lastThrottleTime = System.currentTimeMillis()
@ -28,4 +30,4 @@ class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
const val THROTTLE_MAX = 5500 const val THROTTLE_MAX = 5500
const val THROTTLE_INC = 20 const val THROTTLE_INC = 20
} }
} }

View File

@ -8,10 +8,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import java.io.File
import rx.Observable import rx.Observable
import rx.Single import rx.Single
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
data class ChapterChain(val manga: Manga, val chapters: List<Chapter>) data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
@ -61,7 +61,7 @@ class EHentaiUpdateHelper(context: Context) {
val chainsAsChapters = chains.flatMap { it.chapters } val chainsAsChapters = chains.flatMap { it.chapters }
if(toDiscard.isNotEmpty()) { if (toDiscard.isNotEmpty()) {
var new = false var new = false
// Copy chain chapters to curChapters // Copy chain chapters to curChapters
@ -75,9 +75,9 @@ class EHentaiUpdateHelper(context: Context) {
chain.chapters.map { chapter -> chain.chapters.map { chapter ->
// Convert old style chapters to new style chapters if possible // Convert old style chapters to new style chapters if possible
if(chapter.date_upload <= 0 if (chapter.date_upload <= 0 &&
&& meta?.datePosted != null meta?.datePosted != null &&
&& meta?.title != null) { meta?.title != null) {
chapter.name = meta!!.title!! chapter.name = meta!!.title!!
chapter.date_upload = meta!!.datePosted!! chapter.date_upload = meta!!.datePosted!!
} }
@ -92,7 +92,7 @@ class EHentaiUpdateHelper(context: Context) {
if (existing != null) { if (existing != null) {
existing.read = existing.read || chapter.read existing.read = existing.read || chapter.read
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read) existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
if(newLastPageRead != null && existing.last_page_read <= 0) { if (newLastPageRead != null && existing.last_page_read <= 0) {
existing.last_page_read = newLastPageRead existing.last_page_read = newLastPageRead
} }
existing.bookmark = existing.bookmark || chapter.bookmark existing.bookmark = existing.bookmark || chapter.bookmark
@ -107,7 +107,7 @@ class EHentaiUpdateHelper(context: Context) {
bookmark = chapter.bookmark bookmark = chapter.bookmark
last_page_read = chapter.last_page_read last_page_read = chapter.last_page_read
if(newLastPageRead != null && last_page_read <= 0) { if (newLastPageRead != null && last_page_read <= 0) {
last_page_read = newLastPageRead last_page_read = newLastPageRead
} }
@ -153,7 +153,7 @@ class EHentaiUpdateHelper(context: Context) {
} }
data class GalleryEntry(val gId: String, val gToken: String) { data class GalleryEntry(val gId: String, val gToken: String) {
class Serializer: MemAutoFlushingLookupTable.EntrySerializer<GalleryEntry> { class Serializer : MemAutoFlushingLookupTable.EntrySerializer<GalleryEntry> {
/** /**
* Serialize an entry as a String. * Serialize an entry as a String.
*/ */

View File

@ -20,8 +20,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.system.jobScheduler
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.system.jobScheduler
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.debug.DebugToggles import exh.debug.DebugToggles
@ -31,18 +31,23 @@ import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.await import exh.util.await
import exh.util.cancellable import exh.util.cancellable
import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class EHentaiUpdateWorker: JobService(), CoroutineScope { class EHentaiUpdateWorker : JobService(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + Job() get() = Dispatchers.Default + Job()
@ -215,8 +220,8 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
val (acceptedRoot, discardedRoots, hasNew) = val (acceptedRoot, discardedRoots, hasNew) =
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await() updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
if((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) 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 updatedManga += acceptedRoot.manga
} }
@ -235,7 +240,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
) )
) )
if(updatedManga.isNotEmpty()) { if (updatedManga.isNotEmpty()) {
updateNotifier.showResultNotification(updatedManga) updateNotifier.showResultNotification(updatedManga)
} }
} }
@ -254,10 +259,10 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io()) val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this? val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
return new to db.getChapters(manga).await() return new to db.getChapters(manga).await()
} catch(t: Throwable) { } catch (t: Throwable) {
if(t is EHentai.GalleryNotFoundException) { if (t is EHentai.GalleryNotFoundException) {
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>() val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
if(meta != null) { if (meta != null) {
// Age dead galleries // Age dead galleries
logger.d("Aged %s - notfound", manga.id) logger.d("Aged %s - notfound", manga.id)
meta.aged = true meta.aged = true
@ -286,18 +291,20 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder { private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
return JobInfo.Builder( return JobInfo.Builder(
if(isTest) JOB_ID_UPDATE_BACKGROUND_TEST if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
else JOB_ID_UPDATE_BACKGROUND, componentName()) else JOB_ID_UPDATE_BACKGROUND, componentName())
} }
private fun Context.periodicBackgroundJobInfo(period: Long, private fun Context.periodicBackgroundJobInfo(
requireCharging: Boolean, period: Long,
requireUnmetered: Boolean): JobInfo { requireCharging: Boolean,
requireUnmetered: Boolean
): JobInfo {
return baseBackgroundJobInfo(false) return baseBackgroundJobInfo(false)
.setPeriodic(period) .setPeriodic(period)
.setPersisted(true) .setPersisted(true)
.setRequiredNetworkType( .setRequiredNetworkType(
if(requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY) else JobInfo.NETWORK_TYPE_ANY)
.apply { .apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -321,7 +328,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
fun launchBackgroundTest(context: Context) { fun launchBackgroundTest(context: Context) {
val jobScheduler = context.jobScheduler val jobScheduler = context.jobScheduler
if(jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) { if (jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
logger.e("Failed to schedule background test job!") logger.e("Failed to schedule background test job!")
} else { } else {
logger.d("Successfully scheduled background test job!") logger.d("Successfully scheduled background test job!")
@ -344,7 +351,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
wifiRestriction wifiRestriction
) )
if(context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) { if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
logger.e("Failed to schedule background update job!") logger.e("Failed to schedule background update job!")
} else { } else {
logger.d("Successfully scheduled background update job!") logger.d("Successfully scheduled background update job!")

View File

@ -1,7 +1,7 @@
package exh.eh package exh.eh
data class EHentaiUpdaterStats( data class EHentaiUpdaterStats(
val startTime: Long, val startTime: Long,
val possibleUpdates: Int, val possibleUpdates: Int,
val updateCount: Int val updateCount: Int
) )

View File

@ -1,3 +1,3 @@
package exh.eh package exh.eh
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable): RuntimeException(cause) class GalleryNotUpdatedException(val network: Boolean, cause: Throwable) : RuntimeException(cause)

View File

@ -3,9 +3,6 @@ package exh.eh
import android.util.SparseArray import android.util.SparseArray
import androidx.core.util.AtomicFile import androidx.core.util.AtomicFile
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -13,6 +10,18 @@ import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
/** /**
* In memory Int -> Obj lookup table implementation that * In memory Int -> Obj lookup table implementation that
@ -23,9 +32,9 @@ import kotlin.coroutines.CoroutineContext
* @author nulldev * @author nulldev
*/ */
class MemAutoFlushingLookupTable<T>( class MemAutoFlushingLookupTable<T>(
file: File, file: File,
private val serializer: EntrySerializer<T>, private val serializer: EntrySerializer<T>,
private val debounceTimeMs: Long = 3000 private val debounceTimeMs: Long = 3000
) : CoroutineScope, Closeable { ) : CoroutineScope, Closeable {
/** /**
* The context of this scope. * The context of this scope.
@ -49,7 +58,7 @@ class MemAutoFlushingLookupTable<T>(
private val atomicFile = AtomicFile(file) private val atomicFile = AtomicFile(file)
private val shutdownHook = thread(start = false) { private val shutdownHook = thread(start = false) {
if(!flushed) writeSynchronously() if (!flushed) writeSynchronously()
} }
init { init {
@ -62,9 +71,9 @@ class MemAutoFlushingLookupTable<T>(
var readIter = 0 var readIter = 0
while (true) { while (true) {
val readThisIter = read(targetArray, readIter, byteCount - readIter) val readThisIter = read(targetArray, readIter, byteCount - readIter)
if(readThisIter <= 0) return false // No more data to read if (readThisIter <= 0) return false // No more data to read
readIter += readThisIter readIter += readThisIter
if(readIter == byteCount) return true if (readIter == byteCount) return true
} }
} }
@ -74,16 +83,16 @@ class MemAutoFlushingLookupTable<T>(
atomicFile.openRead().buffered().use { input -> atomicFile.openRead().buffered().use { input ->
val bb = ByteBuffer.allocate(8) val bb = ByteBuffer.allocate(8)
while(true) { while (true) {
if(!input.requireBytes(bb.array(), 8)) break if (!input.requireBytes(bb.array(), 8)) break
val k = bb.getInt(0) val k = bb.getInt(0)
val size = bb.getInt(4) val size = bb.getInt(4)
val strBArr = ByteArray(size) val strBArr = ByteArray(size)
if(!input.requireBytes(strBArr, size)) break if (!input.requireBytes(strBArr, size)) break
table.put(k, serializer.read(strBArr.toString(Charsets.UTF_8))) table.put(k, serializer.read(strBArr.toString(Charsets.UTF_8)))
} }
} }
} catch(e: FileNotFoundException) { } catch (e: FileNotFoundException) {
XLog.d("Lookup table not found!", e) XLog.d("Lookup table not found!", e)
// Ignored // Ignored
} }
@ -97,11 +106,11 @@ class MemAutoFlushingLookupTable<T>(
flushed = false flushed = false
launch { launch {
delay(debounceTimeMs) delay(debounceTimeMs)
if(id != writeCounter) return@launch if (id != writeCounter) return@launch
mutex.withLock { mutex.withLock {
// Second check inside of mutex to prevent dupe writes // Second check inside of mutex to prevent dupe writes
if(id != writeCounter) return@launch if (id != writeCounter) return@launch
withContext(NonCancellable) { withContext(NonCancellable) {
writeSynchronously() writeSynchronously()
@ -118,7 +127,7 @@ class MemAutoFlushingLookupTable<T>(
val fos = atomicFile.startWrite() val fos = atomicFile.startWrite()
try { try {
val out = fos.buffered() val out = fos.buffered()
for(i in 0 until table.size()) { for (i in 0 until table.size()) {
val k = table.keyAt(i) val k = table.keyAt(i)
val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8) val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8)
bb.putInt(0, k) bb.putInt(0, k)
@ -128,7 +137,7 @@ class MemAutoFlushingLookupTable<T>(
} }
out.flush() out.flush()
atomicFile.finishWrite(fos) atomicFile.finishWrite(fos)
} catch(t: Throwable) { } catch (t: Throwable) {
atomicFile.failWrite(fos) atomicFile.failWrite(fos)
throw t throw t
} }
@ -212,4 +221,4 @@ class MemAutoFlushingLookupTable<T>(
private const val INITIAL_SIZE = 1000 private const val INITIAL_SIZE = 1000
private const val ENTRY_SIZE_BYTES = 8 private const val ENTRY_SIZE_BYTES = 8
} }
} }

View File

@ -5,7 +5,7 @@ import io.realm.RealmObject
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import java.util.* import java.util.UUID
@RealmClass @RealmClass
open class FavoriteEntry : RealmObject() { open class FavoriteEntry : RealmObject() {

View File

@ -17,18 +17,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.wifiManager import eu.kanade.tachiyomi.util.system.wifiManager
import exh.* import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.util.ignore import exh.util.ignore
import exh.util.trans import exh.util.trans
import kotlin.concurrent.thread
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import rx.subjects.BehaviorSubject import rx.subjects.BehaviorSubject
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
class FavoritesSyncHelper(val context: Context) { class FavoritesSyncHelper(val context: Context) {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
@ -55,7 +58,7 @@ class FavoritesSyncHelper(val context: Context) {
@Synchronized @Synchronized
fun runSync() { fun runSync() {
if(status.value !is FavoritesSyncStatus.Idle) { if (status.value !is FavoritesSyncStatus.Idle) {
return return
} }
@ -65,8 +68,8 @@ class FavoritesSyncHelper(val context: Context) {
} }
private fun beginSync() { private fun beginSync() {
//Check if logged in // Check if logged in
if(!prefs.enableExhentai().getOrDefault()) { if (!prefs.enableExhentai().getOrDefault()) {
status.onNext(FavoritesSyncStatus.Error("Please log in!")) status.onNext(FavoritesSyncStatus.Error("Please log in!"))
return return
} }
@ -76,9 +79,9 @@ class FavoritesSyncHelper(val context: Context) {
val libraryManga = db.getLibraryMangas().executeAsBlocking() val libraryManga = db.getLibraryMangas().executeAsBlocking()
val seenManga = HashSet<Long>(libraryManga.size) val seenManga = HashSet<Long>(libraryManga.size)
libraryManga.forEach { libraryManga.forEach {
if(it.source != EXH_SOURCE_ID && it.source != EH_SOURCE_ID) return@forEach if (it.source != EXH_SOURCE_ID && it.source != EH_SOURCE_ID) return@forEach
if(it.id in seenManga) { if (it.id in seenManga) {
val inCategories = db.getCategoriesForManga(it).executeAsBlocking() val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
status.onNext(FavoritesSyncStatus.BadLibraryState status.onNext(FavoritesSyncStatus.BadLibraryState
.MangaInMultipleCategories(it, inCategories)) .MangaInMultipleCategories(it, inCategories))
@ -89,20 +92,20 @@ class FavoritesSyncHelper(val context: Context) {
} }
} }
//Download remote favorites // Download remote favorites
val favorites = try { val favorites = try {
status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server")) status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server"))
exh.fetchFavorites() exh.fetchFavorites()
} catch(e: Exception) { } catch (e: Exception) {
status.onNext(FavoritesSyncStatus.Error("Failed to fetch favorites from remote server!")) status.onNext(FavoritesSyncStatus.Error("Failed to fetch favorites from remote server!"))
logger.e( "Could not fetch favorites!", e) logger.e("Could not fetch favorites!", e)
return return
} }
val errorList = mutableListOf<String>() val errorList = mutableListOf<String>()
try { try {
//Take wake + wifi locks // Take wake + wifi locks
ignore { wakeLock?.release() } ignore { wakeLock?.release() }
wakeLock = ignore { wakeLock = ignore {
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
@ -124,20 +127,20 @@ class FavoritesSyncHelper(val context: Context) {
db.inTransaction { db.inTransaction {
status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes")) status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes"))
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first) val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
val localChanges = if(prefs.eh_readOnlySync().getOrDefault()) { val localChanges = if (prefs.eh_readOnlySync().getOrDefault()) {
null //Do not build local changes if they are not going to be applied null // Do not build local changes if they are not going to be applied
} else { } else {
status.onNext(FavoritesSyncStatus.Processing("Calculating local changes")) status.onNext(FavoritesSyncStatus.Processing("Calculating local changes"))
storage.getChangedDbEntries(realm) storage.getChangedDbEntries(realm)
} }
//Apply remote categories // Apply remote categories
status.onNext(FavoritesSyncStatus.Processing("Updating category names")) status.onNext(FavoritesSyncStatus.Processing("Updating category names"))
applyRemoteCategories(errorList, favorites.second) applyRemoteCategories(errorList, favorites.second)
//Apply change sets // Apply change sets
applyChangeSetToLocal(errorList, remoteChanges) applyChangeSetToLocal(errorList, remoteChanges)
if(localChanges != null) if (localChanges != null)
applyChangeSetToRemote(errorList, localChanges) applyChangeSetToRemote(errorList, localChanges)
status.onNext(FavoritesSyncStatus.Processing("Cleaning up")) status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
@ -150,16 +153,16 @@ class FavoritesSyncHelper(val context: Context) {
launchUI { launchUI {
theContext.toast("Sync complete!") theContext.toast("Sync complete!")
} }
} catch(e: IgnoredException) { } catch (e: IgnoredException) {
//Do not display error as this error has already been reported // Do not display error as this error has already been reported
logger.w( "Ignoring exception!", e) logger.w("Ignoring exception!", e)
return return
} catch (e: Exception) { } catch (e: Exception) {
status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}")) status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}"))
logger.e( "Sync error!", e) logger.e("Sync error!", e)
return return
} finally { } finally {
//Release wake + wifi locks // Release wake + wifi locks
ignore { ignore {
wakeLock?.release() wakeLock?.release()
wakeLock = null wakeLock = null
@ -175,7 +178,7 @@ class FavoritesSyncHelper(val context: Context) {
} }
} }
if(errorList.isEmpty()) if (errorList.isEmpty())
status.onNext(FavoritesSyncStatus.Idle()) status.onNext(FavoritesSyncStatus.Idle())
else else
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList)) status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
@ -195,31 +198,31 @@ class FavoritesSyncHelper(val context: Context) {
Category.create(remote).apply { Category.create(remote).apply {
order = index order = index
//Going through categories list from front to back // Going through categories list from front to back
//If category does not exist, list size <= category index // If category does not exist, list size <= category index
//Thus, we can just add it here and not worry about indexing // Thus, we can just add it here and not worry about indexing
newLocalCategories += this newLocalCategories += this
} }
} }
if(local.name != remote) { if (local.name != remote) {
changed = true changed = true
local.name = remote local.name = remote
} }
} }
//Ensure consistent ordering // Ensure consistent ordering
newLocalCategories.forEachIndexed { index, category -> newLocalCategories.forEachIndexed { index, category ->
if(category.order != index) { if (category.order != index) {
changed = true changed = true
category.order = index category.order = index
} }
} }
//Only insert categories if changed // Only insert categories if changed
if(changed) if (changed)
db.insertCategories(newLocalCategories).executeAsBlocking() db.insertCategories(newLocalCategories).executeAsBlocking()
} }
@ -236,10 +239,10 @@ class FavoritesSyncHelper(val context: Context) {
.build()) .build())
.build() .build()
if(!explicitlyRetryExhRequest(10, request)) { if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!" val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
if(prefs.eh_lenientSync().getOrDefault()) { if (prefs.eh_lenientSync().getOrDefault()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.onNext(FavoritesSyncStatus.Error(errorString))
@ -251,7 +254,7 @@ class FavoritesSyncHelper(val context: Context) {
private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean { private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean {
var success = false var success = false
for(i in 1 .. retryCount) { for (i in 1..retryCount) {
try { try {
val resp = exh.client.newCall(request).execute() val resp = exh.client.newCall(request).execute()
@ -260,7 +263,7 @@ class FavoritesSyncHelper(val context: Context) {
break break
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.w( "Sync network error!", e) logger.w("Sync network error!", e)
} }
} }
@ -268,15 +271,15 @@ class FavoritesSyncHelper(val context: Context) {
} }
private fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) { private fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
//Apply removals // Apply removals
if(changeSet.removed.isNotEmpty()) { if (changeSet.removed.isNotEmpty()) {
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server")) status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("ddact", "delete") .add("ddact", "delete")
.add("apply", "Apply") .add("apply", "Apply")
//Add change set to form // Add change set to form
changeSet.removed.forEach { changeSet.removed.forEach {
formBody.add("modifygids[]", it.gid) formBody.add("modifygids[]", it.gid)
} }
@ -286,10 +289,10 @@ class FavoritesSyncHelper(val context: Context) {
.post(formBody.build()) .post(formBody.build())
.build() .build()
if(!explicitlyRetryExhRequest(10, request)) { if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to delete galleries from the remote servers!" val errorString = "Unable to delete galleries from the remote servers!"
if(prefs.eh_lenientSync().getOrDefault()) { if (prefs.eh_lenientSync().getOrDefault()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.onNext(FavoritesSyncStatus.Error(errorString))
@ -298,7 +301,7 @@ class FavoritesSyncHelper(val context: Context) {
} }
} }
//Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server", status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
@ -313,17 +316,17 @@ class FavoritesSyncHelper(val context: Context) {
private fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) { private fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) {
val removedManga = mutableListOf<Manga>() val removedManga = mutableListOf<Manga>()
//Apply removals // Apply removals
changeSet.removed.forEachIndexed { index, it -> changeSet.removed.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library")) status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library"))
val url = it.getUrl() val url = it.getUrl()
//Consider both EX and EH sources // Consider both EX and EH sources
listOf(db.getManga(url, EXH_SOURCE_ID), listOf(db.getManga(url, EXH_SOURCE_ID),
db.getManga(url, EH_SOURCE_ID)).forEach { db.getManga(url, EH_SOURCE_ID)).forEach {
val manga = it.executeAsBlocking() val manga = it.executeAsBlocking()
if(manga?.favorite == true) { if (manga?.favorite == true) {
manga.favorite = false manga.favorite = false
db.updateMangaFavorite(manga).executeAsBlocking() db.updateMangaFavorite(manga).executeAsBlocking()
removedManga += manga removedManga += manga
@ -339,7 +342,7 @@ class FavoritesSyncHelper(val context: Context) {
val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>() val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>()
val categories = db.getCategories().executeAsBlocking() val categories = db.getCategories().executeAsBlocking()
//Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library", status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
@ -347,14 +350,14 @@ class FavoritesSyncHelper(val context: Context) {
throttleManager.throttle() throttleManager.throttle()
//Import using gallery adder // Import using gallery adder
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}", val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
true, true,
exh, exh,
throttleManager::throttle) throttleManager::throttle)
if(result is GalleryAddEvent.Fail) { if (result is GalleryAddEvent.Fail) {
if(result is GalleryAddEvent.Fail.NotFound) { if (result is GalleryAddEvent.Fail.NotFound) {
XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl()) XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl())
// Skip this gallery, it no longer exists // Skip this gallery, it no longer exists
return@forEachIndexed return@forEachIndexed
@ -365,13 +368,13 @@ class FavoritesSyncHelper(val context: Context) {
is GalleryAddEvent.Fail.UnknownType -> "'${it.title}' (${result.galleryUrl}) is not a valid gallery!" is GalleryAddEvent.Fail.UnknownType -> "'${it.title}' (${result.galleryUrl}) is not a valid gallery!"
} }
if(prefs.eh_lenientSync().getOrDefault()) { if (prefs.eh_lenientSync().getOrDefault()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.onNext(FavoritesSyncStatus.Error(errorString))
throw IgnoredException() throw IgnoredException()
} }
} else if(result is GalleryAddEvent.Success) { } else if (result is GalleryAddEvent.Success) {
insertedMangaCategories += MangaCategory.create(result.manga, insertedMangaCategories += MangaCategory.create(result.manga,
categories[it.category]) to result.manga categories[it.category]) to result.manga
} }
@ -385,8 +388,8 @@ class FavoritesSyncHelper(val context: Context) {
} }
} }
fun needWarnThrottle() fun needWarnThrottle() =
= throttleManager.throttleTime >= THROTTLE_WARN throttleManager.throttleTime >= THROTTLE_WARN
class IgnoredException : RuntimeException() class IgnoredException : RuntimeException()
@ -399,12 +402,14 @@ sealed class FavoritesSyncStatus(val message: String) {
class Error(message: String) : FavoritesSyncStatus(message) class Error(message: String) : FavoritesSyncStatus(message)
class Idle : FavoritesSyncStatus("Waiting for sync to start") class Idle : FavoritesSyncStatus("Waiting for sync to start")
sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) { sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) {
class MangaInMultipleCategories(val manga: Manga, class MangaInMultipleCategories(
val categories: List<Category>): val manga: Manga,
val categories: List<Category>
) :
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!") BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
} }
class Initializing : FavoritesSyncStatus("Initializing sync") class 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." "$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
else else
message) message)

View File

@ -20,8 +20,8 @@ class LocalFavoritesStorage {
fun getRealm() = Realm.getInstance(realmConfig) fun getRealm() = Realm.getInstance(realmConfig)
fun getChangedDbEntries(realm: Realm) fun getChangedDbEntries(realm: Realm) =
= getChangedEntries(realm, getChangedEntries(realm,
parseToFavoriteEntries( parseToFavoriteEntries(
loadDbCategories( loadDbCategories(
db.getFavoriteMangas() db.getFavoriteMangas()
@ -31,8 +31,8 @@ class LocalFavoritesStorage {
) )
) )
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
= getChangedEntries(realm, getChangedEntries(realm,
parseToFavoriteEntries( parseToFavoriteEntries(
entries.asSequence().map { entries.asSequence().map {
Pair(it.fav, it.manga.apply { Pair(it.fav, it.manga.apply {
@ -51,10 +51,10 @@ class LocalFavoritesStorage {
) )
) )
//Delete old snapshot // Delete old snapshot
realm.delete(FavoriteEntry::class.java) realm.delete(FavoriteEntry::class.java)
//Insert new snapshots // Insert new snapshots
realm.copyToRealm(dbMangas.toList()) realm.copyToRealm(dbMangas.toList())
} }
@ -80,18 +80,18 @@ class LocalFavoritesStorage {
return ChangeSet(added, removed) return ChangeSet(added, removed)
} }
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
= where(FavoriteEntry::class.java) where(FavoriteEntry::class.java)
.equalTo(FavoriteEntry::gid.name, entry.gid) .equalTo(FavoriteEntry::gid.name, entry.gid)
.equalTo(FavoriteEntry::token.name, entry.token) .equalTo(FavoriteEntry::token.name, entry.token)
.equalTo(FavoriteEntry::category.name, entry.category) .equalTo(FavoriteEntry::category.name, entry.category)
.findFirst() .findFirst()
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
= list.find { list.find {
it.gid == entry.gid it.gid == entry.gid &&
&& it.token == entry.token it.token == entry.token &&
&& it.category == entry.category it.category == entry.category
} }
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> { private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
@ -105,8 +105,8 @@ class LocalFavoritesStorage {
} }
} }
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
= manga.filter { manga.filter {
validateDbManga(it.second) validateDbManga(it.second)
}.mapNotNull { }.mapNotNull {
FavoriteEntry().apply { FavoriteEntry().apply {
@ -115,18 +115,20 @@ class LocalFavoritesStorage {
token = EHentaiSearchMetadata.galleryToken(it.second.url) token = EHentaiSearchMetadata.galleryToken(it.second.url)
category = it.first category = it.first
if(this.category > MAX_CATEGORIES) if (this.category > MAX_CATEGORIES)
return@mapNotNull null return@mapNotNull null
} }
} }
private fun validateDbManga(manga: Manga) private fun validateDbManga(manga: Manga) =
= manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
companion object { companion object {
const val MAX_CATEGORIES = 9 const val MAX_CATEGORIES = 9
} }
} }
data class ChangeSet(val added: List<FavoriteEntry>, data class ChangeSet(
val removed: List<FavoriteEntry>) val added: List<FavoriteEntry>,
val removed: List<FavoriteEntry>
)

View File

@ -4,42 +4,46 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
import java.security.MessageDigest
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.vepta.vdm.ByteCursor import org.vepta.vdm.ByteCursor
import rx.Observable import rx.Observable
import rx.Single import rx.Single
import java.security.MessageDigest
private typealias HashedTerm = ByteArray private typealias HashedTerm = ByteArray
private data class DataPair(val offset: Long, val length: Int) private data class DataPair(val offset: Long, val length: Int)
private data class Node(val keys: List<ByteArray>, private data class Node(
val datas: List<DataPair>, val keys: List<ByteArray>,
val subnodeAddresses: List<Long>) val datas: List<DataPair>,
val subnodeAddresses: List<Long>
)
/** /**
* Kotlin port of the hitomi.la search algorithm * Kotlin port of the hitomi.la search algorithm
* @author NerdNumber9 * @author NerdNumber9
*/ */
class HitomiNozomi(private val client: OkHttpClient, class HitomiNozomi(
private val tagIndexVersion: Long, private val client: OkHttpClient,
private val galleriesIndexVersion: Long) { private val tagIndexVersion: Long,
private val galleriesIndexVersion: Long
) {
fun getGalleryIdsForQuery(query: String): Single<List<Int>> { fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
val replacedQuery = query.replace('_', ' ') val replacedQuery = query.replace('_', ' ')
if(':' in replacedQuery) { if (':' in replacedQuery) {
val sides = replacedQuery.split(':') val sides = replacedQuery.split(':')
val namespace = sides[0] val namespace = sides[0]
var tag = sides[1] var tag = sides[1]
var area: String? = namespace var area: String? = namespace
var language = "all" var language = "all"
if(namespace == "female" || namespace == "male") { if (namespace == "female" || namespace == "male") {
area = "tag" area = "tag"
tag = replacedQuery tag = replacedQuery
} else if(namespace == "language") { } else if (namespace == "language") {
area = null area = null
language = tag language = tag
tag = "index" tag = "index"
@ -52,7 +56,7 @@ class HitomiNozomi(private val client: OkHttpClient,
val field = "galleries" val field = "galleries"
return getNodeAtAddress(field, 0).flatMap { node -> return getNodeAtAddress(field, 0).flatMap { node ->
if(node == null) { if (node == null) {
Single.just(null) Single.just(null)
} else { } else {
BSearch(field, key, node).flatMap { data -> BSearch(field, key, node).flatMap { data ->
@ -67,12 +71,12 @@ class HitomiNozomi(private val client: OkHttpClient,
} }
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> { private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
if(data == null) if (data == null)
return Single.just(emptyList()) return Single.just(emptyList())
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data" val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
val (offset, length) = data val (offset, length) = data
if(length > 100000000 || length <= 0) if (length > 100000000 || length <= 0)
return Single.just(emptyList()) return Single.just(emptyList())
return client.newCall(rangedGet(url, offset, offset + length - 1)) return client.newCall(rangedGet(url, offset, offset + length - 1))
@ -82,7 +86,7 @@ class HitomiNozomi(private val client: OkHttpClient,
} }
.onErrorReturn { ByteArray(0) } .onErrorReturn { ByteArray(0) }
.map { inbuf -> .map { inbuf ->
if(inbuf.isEmpty()) if (inbuf.isEmpty())
return@map emptyList<Int>() return@map emptyList<Int>()
val view = ByteCursor(inbuf) val view = ByteCursor(inbuf)
@ -90,13 +94,13 @@ class HitomiNozomi(private val client: OkHttpClient,
val expectedLength = numberOfGalleryIds * 4 + 4 val expectedLength = numberOfGalleryIds * 4 + 4
if(numberOfGalleryIds > 10000000 if (numberOfGalleryIds > 10000000 ||
|| numberOfGalleryIds <= 0 numberOfGalleryIds <= 0 ||
|| inbuf.size != expectedLength) { inbuf.size != expectedLength) {
return@map emptyList<Int>() return@map emptyList<Int>()
} }
(1 .. numberOfGalleryIds).map { (1..numberOfGalleryIds).map {
view.nextInt() view.nextInt()
} }
}.toSingle() }.toSingle()
@ -105,12 +109,12 @@ class HitomiNozomi(private val client: OkHttpClient,
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> { private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int { fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
val top = Math.min(dv1.size, dv2.size) val top = Math.min(dv1.size, dv2.size)
for(i in 0 until top) { for (i in 0 until top) {
val dv1i = dv1[i].toInt() and 0xFF val dv1i = dv1[i].toInt() and 0xFF
val dv2i = dv2[i].toInt() and 0xFF val dv2i = dv2[i].toInt() and 0xFF
if(dv1i < dv2i) if (dv1i < dv2i)
return -1 return -1
else if(dv1i > dv2i) else if (dv1i > dv2i)
return 1 return 1
} }
return 0 return 0
@ -119,9 +123,9 @@ class HitomiNozomi(private val client: OkHttpClient,
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> { fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
var cmpResult = -1 var cmpResult = -1
var lastI = 0 var lastI = 0
for(nodeKey in node.keys) { for (nodeKey in node.keys) {
cmpResult = compareByteArrays(key, nodeKey) cmpResult = compareByteArrays(key, nodeKey)
if(cmpResult <= 0) break if (cmpResult <= 0) break
lastI++ lastI++
} }
return (cmpResult == 0) to lastI return (cmpResult == 0) to lastI
@ -133,14 +137,14 @@ class HitomiNozomi(private val client: OkHttpClient,
} }
} }
if(node == null || node.keys.isEmpty()) { if (node == null || node.keys.isEmpty()) {
return Single.just(null) return Single.just(null)
} }
val (there, where) = locateKey(key, node) val (there, where) = locateKey(key, node)
if(there) { if (there) {
return Single.just(node.datas[where]) return Single.just(node.datas[where])
} else if(isLeaf(node)) { } else if (isLeaf(node)) {
return Single.just(null) return Single.just(null)
} }
@ -154,20 +158,20 @@ class HitomiNozomi(private val client: OkHttpClient,
val numberOfKeys = view.nextInt() val numberOfKeys = view.nextInt()
val keys = (1 .. numberOfKeys).map { val keys = (1..numberOfKeys).map {
val keySize = view.nextInt() val keySize = view.nextInt()
view.next(keySize) view.next(keySize)
} }
val numberOfDatas = view.nextInt() val numberOfDatas = view.nextInt()
val datas = (1 .. numberOfDatas).map { val datas = (1..numberOfDatas).map {
val offset = view.nextLong() val offset = view.nextLong()
val length = view.nextInt() val length = view.nextInt()
DataPair(offset, length) DataPair(offset, length)
} }
val numberOfSubnodeAddresses = B + 1 val numberOfSubnodeAddresses = B + 1
val subnodeAddresses = (1 .. numberOfSubnodeAddresses).map { val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
view.nextLong() view.nextLong()
} }
@ -176,7 +180,7 @@ class HitomiNozomi(private val client: OkHttpClient,
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> { private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index" var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index"
if(field == "galleries") { if (field == "galleries") {
url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index" url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index"
} }
@ -187,7 +191,7 @@ class HitomiNozomi(private val client: OkHttpClient,
} }
.onErrorReturn { ByteArray(0) } .onErrorReturn { ByteArray(0) }
.map { nodedata -> .map { nodedata ->
if(nodedata.isNotEmpty()) { if (nodedata.isNotEmpty()) {
decodeNode(nodedata) decodeNode(nodedata)
} else null } else null
}.toSingle() }.toSingle()
@ -195,7 +199,7 @@ class HitomiNozomi(private val client: OkHttpClient,
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> { fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$tag-$language$NOZOMI_EXTENSION" var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$tag-$language$NOZOMI_EXTENSION"
if(area != null) { if (area != null) {
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION" nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
} }
@ -206,7 +210,7 @@ class HitomiNozomi(private val client: OkHttpClient,
.map { resp -> .map { resp ->
val body = resp.body!!.bytes() val body = resp.body!!.bytes()
val cursor = ByteCursor(body) val cursor = ByteCursor(body)
(1 .. body.size / 4).map { (1..body.size / 4).map {
cursor.nextInt() cursor.nextInt()
} }
}.toSingle() }.toSingle()
@ -234,11 +238,10 @@ class HitomiNozomi(private val client: OkHttpClient,
.build()) .build())
} }
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> { fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}")) return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
.asObservableSuccess() .asObservableSuccess()
.map { it.body!!.string().toLong() } .map { it.body!!.string().toLong() }
} }
} }
} }

View File

@ -9,17 +9,17 @@ class CrashlyticsPrinter(private val logLevel: Int) : Printer {
* Print log in new line. * Print log in new line.
* *
* @param logLevel the level of log * @param logLevel the level of log
* @param tag the tag of log * @param tag the tag of log
* @param msg the msg of log * @param msg the msg of log
*/ */
override fun println(logLevel: Int, tag: String?, msg: String?) { override fun println(logLevel: Int, tag: String?, msg: String?) {
if(logLevel >= this.logLevel) { if (logLevel >= this.logLevel) {
try { try {
Crashlytics.log(logLevel, tag, msg) Crashlytics.log(logLevel, tag, msg)
} catch (t: Throwable) { } catch (t: Throwable) {
// Crash in debug if shit like this happens // Crash in debug if shit like this happens
if(BuildConfig.DEBUG) throw t if (BuildConfig.DEBUG) throw t
} }
} }
} }
} }

View File

@ -4,11 +4,11 @@ import android.content.Context
import android.text.Html import android.text.Html
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.ms_square.debugoverlay.DataObserver import com.ms_square.debugoverlay.DataObserver
import com.ms_square.debugoverlay.OverlayModule import com.ms_square.debugoverlay.OverlayModule
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import android.widget.LinearLayout
import android.widget.TextView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -61,5 +61,5 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().getOrDefault().asEnabledString()} <b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().getOrDefault().asEnabledString()}
""".trimIndent() """.trimIndent()
private fun Boolean.asEnabledString() = if(this) "enabled" else "disabled" private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
} }

View File

@ -23,4 +23,4 @@ enum class EHLogLevel(val description: String) {
return curLogLevel!! >= requiredLogLevel.ordinal return curLogLevel!! >= requiredLogLevel.ordinal
} }
} }
} }

View File

@ -2,7 +2,7 @@ package exh.log
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
fun OkHttpClient.Builder.maybeInjectEHLogger(): OkHttpClient.Builder { //TODO - un-break this fun OkHttpClient.Builder.maybeInjectEHLogger(): OkHttpClient.Builder { // TODO - un-break this
/* if(false &&EHLogLevel.shouldLog(EHLogLevel.EXTREME)) { /* if(false &&EHLogLevel.shouldLog(EHLogLevel.EXTREME)) {
val xLogger = XLog.tag("EHNetwork") val xLogger = XLog.tag("EHNetwork")
.nst() .nst()

View File

@ -1,7 +1,7 @@
package exh.metadata package exh.metadata
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
/** /**
* Metadata utils * Metadata utils
@ -35,13 +35,12 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
return null return null
} }
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
null null
else else
this this
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) { fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) {
forEach { action(it.key, it.value) } forEach { action(it.key, it.value) }
} }

View File

@ -4,12 +4,14 @@ import android.net.Uri
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.* import exh.metadata.EX_DATE_FORMAT
import exh.metadata.ONGOING_SUFFIX
import exh.metadata.humanReadableByteCount
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign import exh.plusAssign
import java.util.Date
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.*
class EHentaiSearchMetadata : RaisedSearchMetadata() { class EHentaiSearchMetadata : RaisedSearchMetadata() {
var gId: String? var gId: String?
@ -27,7 +29,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
var datePosted: Long? = null var datePosted: Long? = null
var parent: String? = null var parent: String? = null
var visible: String? = null //Not a boolean var visible: String? = null // Not a boolean
var language: String? = null var language: String? = null
var translated: Boolean? = null var translated: Boolean? = null
var size: Long? = null var size: Long? = null
@ -47,23 +49,23 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
} }
thumbnailUrl?.let { manga.thumbnail_url = it } thumbnailUrl?.let { manga.thumbnail_url = it }
//No title bug? // No title bug?
val titleObj = if(Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault()) val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
altTitle ?: title altTitle ?: title
else else
title title
titleObj?.let { manga.title = it } titleObj?.let { manga.title = it }
//Set artist (if we can find one) // Set artist (if we can find one)
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let { tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name }) if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
} }
//Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes // Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
//We default to completed // We default to completed
manga.status = SManga.COMPLETED manga.status = SManga.COMPLETED
title?.let { t -> title?.let { t ->
ONGOING_SUFFIX.find { ONGOING_SUFFIX.find {
@ -73,7 +75,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
} }
} }
//Build a nice looking description out of what we know // Build a nice looking description out of what we know
val titleDesc = StringBuilder() val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" } title?.let { titleDesc += "Title: $it\n" }
altTitle?.let { titleDesc += "Alternate Title: $it\n" } altTitle?.let { titleDesc += "Alternate Title: $it\n" }
@ -85,7 +87,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
visible?.let { detailsDesc += "Visible: $it\n" } visible?.let { detailsDesc += "Visible: $it\n" }
language?.let { language?.let {
detailsDesc += "Language: $it" detailsDesc += "Language: $it"
if(translated == true) detailsDesc += " TR" if (translated == true) detailsDesc += " TR"
detailsDesc += "\n" detailsDesc += "\n"
} }
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" } size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
@ -114,10 +116,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
const val EH_GENRE_NAMESPACE = "genre" const val EH_GENRE_NAMESPACE = "genre"
private const val EH_ARTIST_NAMESPACE = "artist" private const val EH_ARTIST_NAMESPACE = "artist"
private fun splitGalleryUrl(url: String) private fun splitGalleryUrl(url: String) =
= url.let { url.let {
//Only parse URL if is full URL // Only parse URL if is full URL
val pathSegments = if(it.startsWith("http")) val pathSegments = if (it.startsWith("http"))
Uri.parse(it).pathSegments Uri.parse(it).pathSegments
else else
it.split('/') it.split('/')
@ -129,10 +131,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
fun galleryToken(url: String) = fun galleryToken(url: String) =
splitGalleryUrl(url)[2] splitGalleryUrl(url)[2]
fun normalizeUrl(url: String) fun normalizeUrl(url: String) =
= idAndTokenToUrl(galleryId(url), galleryToken(url)) idAndTokenToUrl(galleryId(url), galleryToken(url))
fun idAndTokenToUrl(id: String, token: String) fun idAndTokenToUrl(id: String, token: String) =
= "/g/$id/$token/?nw=always" "/g/$id/$token/?nw=always"
} }
} }

View File

@ -34,7 +34,6 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
manga.description = listOf(titleDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -47,4 +46,4 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
const val TAGS_NAMESPACE = "tags" const val TAGS_NAMESPACE = "tags"
const val ARTIST_NAMESPACE = "artist" const val ARTIST_NAMESPACE = "artist"
} }
} }

View File

@ -21,7 +21,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
} }
// Guess thumbnail URL if manga does not have thumbnail URL // Guess thumbnail URL if manga does not have thumbnail URL
if(manga.thumbnail_url.isNullOrBlank()) { if (manga.thumbnail_url.isNullOrBlank()) {
manga.thumbnail_url = guessThumbnailUrl(hbId.toString()) manga.thumbnail_url = guessThumbnailUrl(hbId.toString())
} }
@ -49,4 +49,4 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed" return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
} }
} }
} }

View File

@ -27,7 +27,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
manga.artist = artist manga.artist = artist
manga.author = artist manga.author = artist
//Not available // Not available
manga.status = SManga.UNKNOWN manga.status = SManga.UNKNOWN
val detailsDesc = "Title: $title\n" + val detailsDesc = "Title: $title\n" +
@ -49,7 +49,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
const val BASE_URL = "https://hentai.cafe" const val BASE_URL = "https://hentai.cafe"
fun hcIdFromUrl(url: String) fun hcIdFromUrl(url: String) =
= url.split("/").last { it.isNotBlank() } url.split("/").last { it.isNotBlank() }
} }
} }

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.EX_DATE_FORMAT import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign import exh.plusAssign
import java.util.* import java.util.Date
class HitomiSearchMetadata: RaisedSearchMetadata() { class HitomiSearchMetadata : RaisedSearchMetadata() {
var url get() = hlId?.let { urlFromHlId(it) } var url get() = hlId?.let { urlFromHlId(it) }
set(a) { set(a) {
a?.let { a?.let {
@ -62,10 +62,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
detailsDesc += "Language: ${it.capitalize()}\n" detailsDesc += "Language: ${it.capitalize()}\n"
} }
if(series.isNotEmpty()) if (series.isNotEmpty())
detailsDesc += "Series: ${series.joinToString()}\n" detailsDesc += "Series: ${series.joinToString()}\n"
if(characters.isNotEmpty()) if (characters.isNotEmpty())
detailsDesc += "Characters: ${characters.joinToString()}\n" detailsDesc += "Characters: ${characters.joinToString()}\n"
uploadDate?.let { uploadDate?.let {
@ -74,7 +74,7 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
manga.status = SManga.UNKNOWN manga.status = SManga.UNKNOWN
//Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
@ -92,10 +92,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
const val LTN_BASE_URL = "https://ltn.hitomi.la" const val LTN_BASE_URL = "https://ltn.hitomi.la"
const val BASE_URL = "https://hitomi.la" const val BASE_URL = "https://hitomi.la"
fun hlIdFromUrl(url: String) fun hlIdFromUrl(url: String) =
= url.split('/').last().split('-').last().substringBeforeLast('.') url.split('/').last().split('-').last().substringBeforeLast('.')
fun urlFromHlId(id: String) fun urlFromHlId(id: String) =
= "$BASE_URL/galleries/$id.html" "$BASE_URL/galleries/$id.html"
} }
} }

View File

@ -3,12 +3,14 @@ package exh.metadata.metadata
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.* import exh.metadata.EX_DATE_FORMAT
import exh.metadata.ONGOING_SUFFIX
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.nullIfBlank
import exh.plusAssign import exh.plusAssign
import java.util.Date
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.*
class NHentaiSearchMetadata : RaisedSearchMetadata() { class NHentaiSearchMetadata : RaisedSearchMetadata() {
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) } var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
@ -39,10 +41,10 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
override fun copyTo(manga: SManga) { override fun copyTo(manga: SManga) {
nhId?.let { manga.url = nhIdToPath(it) } nhId?.let { manga.url = nhIdToPath(it) }
if(mediaId != null) { if (mediaId != null) {
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault() val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
typeToExtension(if(hqThumbs) coverImageType else thumbnailImageType)?.let { 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" "cover"
else "thumb"}.$it" else "thumb"}.$it"
} }
@ -50,21 +52,21 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!! manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
//Set artist (if we can find one) // Set artist (if we can find one)
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name }) if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
} }
var category: String? = null var category: String? = null
tags.filter { it.namespace == NHENTAI_CATEGORIES_NAMESPACE }.let { tags.filter { it.namespace == NHENTAI_CATEGORIES_NAMESPACE }.let {
if(it.isNotEmpty()) category = it.joinToString(transform = { it.name }) if (it.isNotEmpty()) category = it.joinToString(transform = { it.name })
} }
//Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes // Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
//We default to completed // We default to completed
manga.status = SManga.COMPLETED manga.status = SManga.COMPLETED
englishTitle?.let { t -> englishTitle?.let { t ->
ONGOING_SUFFIX.find { ONGOING_SUFFIX.find {
@ -106,14 +108,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
private const val NHENTAI_CATEGORIES_NAMESPACE = "category" private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
fun typeToExtension(t: String?) = fun typeToExtension(t: String?) =
when(t) { when (t) {
"p" -> "png" "p" -> "png"
"j" -> "jpg" "j" -> "jpg"
else -> null else -> null
} }
fun nhUrlToId(url: String) fun nhUrlToId(url: String) =
= url.split("/").last { it.isNotBlank() }.toLong() url.split("/").last { it.isNotBlank() }.toLong()
fun nhIdToPath(id: Long) = "/g/$id/" fun nhIdToPath(id: Long) = "/g/$id/"
} }

View File

@ -41,7 +41,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
manga.title = it manga.title = it
titleDesc += "Title: $it\n" titleDesc += "Title: $it\n"
} }
if(altTitles.isNotEmpty()) if (altTitles.isNotEmpty())
titleDesc += "Alternate Titles: \n" + altTitles titleDesc += "Alternate Titles: \n" + altTitles
.joinToString(separator = "\n", postfix = "\n") { .joinToString(separator = "\n", postfix = "\n") {
"$it" "$it"
@ -58,7 +58,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
} }
status?.let { status?.let {
manga.status = when(it) { manga.status = when (it) {
"Ongoing" -> SManga.ONGOING "Ongoing" -> SManga.ONGOING
"Completed", "Suspended" -> SManga.COMPLETED "Completed", "Suspended" -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
@ -70,7 +70,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
detailsDesc += "Rating: %.2\n".format(it) detailsDesc += "Rating: %.2\n".format(it)
} }
//Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
@ -80,15 +80,14 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
private const val TITLE_TYPE_MAIN = 0 private const val TITLE_TYPE_MAIN = 0
private const val TITLE_TYPE_ALT = 1 private const val TITLE_TYPE_ALT = 1
const val TAG_TYPE_DEFAULT = 0 const val TAG_TYPE_DEFAULT = 0
private fun splitGalleryUrl(url: String) private fun splitGalleryUrl(url: String) =
= url.let { url.let {
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
} }
@ -97,13 +96,13 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
} }
enum class PervEdenLang(val id: Long) { enum class PervEdenLang(val id: Long) {
//DO NOT RENAME THESE TO CAPITAL LETTERS! The enum names are used to build URLs // DO NOT RENAME THESE TO CAPITAL LETTERS! The enum names are used to build URLs
en(PERV_EDEN_EN_SOURCE_ID), en(PERV_EDEN_EN_SOURCE_ID),
it(PERV_EDEN_IT_SOURCE_ID); it(PERV_EDEN_IT_SOURCE_ID);
companion object { companion object {
fun source(id: Long) fun source(id: Long) =
= values().find { it.id == id } values().find { it.id == id }
?: throw IllegalArgumentException("Unknown source ID: $id!") ?: throw IllegalArgumentException("Unknown source ID: $id!")
} }
} }

View File

@ -47,7 +47,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
altTitle?.let { titleDesc += "Japanese Title: $it\n" } altTitle?.let { titleDesc += "Japanese Title: $it\n" }
val detailsDesc = StringBuilder() val detailsDesc = StringBuilder()
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n"} (uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
pages?.let { detailsDesc += "Length: $it pages\n" } pages?.let { detailsDesc += "Length: $it pages\n" }
fileSize?.let { detailsDesc += "Size: $it\n" } fileSize?.let { detailsDesc += "Size: $it\n" }
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" } ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
@ -69,4 +69,4 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://pururin.io" val BASE_URL = "https://pururin.io"
} }
} }

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.EX_DATE_FORMAT import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign import exh.plusAssign
import java.util.* import java.util.Date
class TsuminoSearchMetadata : RaisedSearchMetadata() { class TsuminoSearchMetadata : RaisedSearchMetadata() {
var tmId: Int? = null var tmId: Int? = null
@ -51,15 +51,15 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
collection?.let { detailsDesc += "Collection: $it\n" } collection?.let { detailsDesc += "Collection: $it\n" }
group?.let { detailsDesc += "Group: $it\n" } group?.let { detailsDesc += "Group: $it\n" }
val parodiesString = parody.joinToString() val parodiesString = parody.joinToString()
if(parodiesString.isNotEmpty()) { if (parodiesString.isNotEmpty()) {
detailsDesc += "Parody: $parodiesString\n" detailsDesc += "Parody: $parodiesString\n"
} }
val charactersString = character.joinToString() val charactersString = character.joinToString()
if(charactersString.isNotEmpty()) { if (charactersString.isNotEmpty()) {
detailsDesc += "Character: $charactersString\n" detailsDesc += "Character: $charactersString\n"
} }
//Copy tags -> genres // Copy tags -> genres
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
@ -76,8 +76,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://www.tsumino.com" val BASE_URL = "https://www.tsumino.com"
fun tmIdFromUrl(url: String) fun tmIdFromUrl(url: String) =
= Uri.parse(url).lastPathSegment Uri.parse(url).lastPathSegment
fun mangaUrlFromId(id: String) = "/Book/Info/$id" fun mangaUrlFromId(id: String) = "/Book/Info/$id"

View File

@ -5,19 +5,19 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import exh.metadata.sql.models.SearchMetadata import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle import exh.metadata.sql.models.SearchTitle
import kotlin.reflect.KClass
import rx.Completable import rx.Completable
import rx.Single import rx.Single
import kotlin.reflect.KClass
data class FlatMetadata( data class FlatMetadata(
val metadata: SearchMetadata, val metadata: SearchMetadata,
val tags: List<SearchTag>, val tags: List<SearchTag>,
val titles: List<SearchTitle> val titles: List<SearchTitle>
) { ) {
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class) inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
= RaisedSearchMetadata.raiseFlattenGson RaisedSearchMetadata.raiseFlattenGson
.fromJson(metadata.extra, clazz.java).apply { .fromJson(metadata.extra, clazz.java).apply {
fillBaseFields(this@FlatMetadata) fillBaseFields(this@FlatMetadata)
} }
@ -27,7 +27,7 @@ fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<Fla
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
val single = Single.fromCallable { val single = Single.fromCallable {
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking() val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
if(meta != null) { if (meta != null) {
val tags = getSearchTagsForManga(mangaId).executeAsBlocking() val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking() val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
@ -92,4 +92,4 @@ fun DatabaseHelper.insertFlatMetadata(flatMetadata: FlatMetadata) = Completable.
setSearchTagsForManga(flatMetadata.metadata.mangaId, flatMetadata.tags) setSearchTagsForManga(flatMetadata.metadata.mangaId, flatMetadata.tags)
setSearchTitlesForManga(flatMetadata.metadata.mangaId, flatMetadata.titles) setSearchTitlesForManga(flatMetadata.metadata.mangaId, flatMetadata.titles)
} }
} }

View File

@ -30,18 +30,18 @@ abstract class RaisedSearchMetadata {
fun replaceTitleOfType(type: Int, newTitle: String?) { fun replaceTitleOfType(type: Int, newTitle: String?) {
titles.removeAll { it.type == type } titles.removeAll { it.type == type }
if(newTitle != null) titles += RaisedTitle(newTitle, type) if (newTitle != null) titles += RaisedTitle(newTitle, type)
} }
abstract fun copyTo(manga: SManga) abstract fun copyTo(manga: SManga)
fun tagsToGenreString() fun tagsToGenreString() =
= tags.filter { it.type != TAG_TYPE_VIRTUAL } tags.filter { it.type != TAG_TYPE_VIRTUAL }
.joinToString { (if(it.namespace != null) "${it.namespace}: " else "") + it.name } .joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
fun tagsToDescription() fun tagsToDescription() =
= StringBuilder("Tags:\n").apply { StringBuilder("Tags:\n").apply {
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags' // BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy { val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
it.namespace it.namespace
}.entries }.entries
@ -49,7 +49,7 @@ abstract class RaisedSearchMetadata {
groupedTags.forEach { namespace, tags -> groupedTags.forEach { namespace, tags ->
if (tags.isNotEmpty()) { if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" }) val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
if(namespace != null) { if (namespace != null) {
this += "" this += ""
this += namespace this += namespace
this += ": " this += ": "
@ -125,8 +125,8 @@ abstract class RaisedSearchMetadata {
* @param property the metadata for the property. * @param property the metadata for the property.
* @return the property value. * @return the property value.
*/ */
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
= thisRef.getTitleOfType(type) thisRef.getTitleOfType(type)
/** /**
* Sets the value of the property for the given object. * Sets the value of the property for the given object.
@ -134,8 +134,8 @@ abstract class RaisedSearchMetadata {
* @param property the metadata for the property. * @param property the metadata for the property.
* @param value the value to set. * @param value the value to set.
*/ */
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
= thisRef.replaceTitleOfType(type, value) thisRef.replaceTitleOfType(type, value)
} }
} }
} }

View File

@ -1,5 +1,7 @@
package exh.metadata.metadata.base package exh.metadata.metadata.base
data class RaisedTag(val namespace: String?, data class RaisedTag(
val name: String, val namespace: String?,
val type: Int) val name: String,
val type: Int
)

View File

@ -1,6 +1,6 @@
package exh.metadata.metadata.base package exh.metadata.metadata.base
data class RaisedTitle( data class RaisedTitle(
val title: String, val title: String,
val type: Int = 0 val type: Int = 0
) )

View File

@ -2,20 +2,20 @@ package exh.metadata.sql.models
data class SearchMetadata( data class SearchMetadata(
// Manga ID this gallery is linked to // Manga ID this gallery is linked to
val mangaId: Long, val mangaId: Long,
// Gallery uploader // Gallery uploader
val uploader: String?, val uploader: String?,
// Extra data attached to this metadata, in JSON format // Extra data attached to this metadata, in JSON format
val extra: String, val extra: String,
// Indexed extra data attached to this metadata // Indexed extra data attached to this metadata
val indexedExtra: String?, val indexedExtra: String?,
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema // The version of this metadata's extra. Used to track changes to the 'extra' field's schema
val extraVersion: Int val extraVersion: Int
) { ) {
// Transient information attached to this piece of metadata, useful for caching // Transient information attached to this piece of metadata, useful for caching
var transientCache: Map<String, Any>? = null var transientCache: Map<String, Any>? = null
} }

View File

@ -2,17 +2,17 @@ package exh.metadata.sql.models
data class SearchTag( data class SearchTag(
// Tag identifier, unique // Tag identifier, unique
val id: Long?, val id: Long?,
// Metadata this tag is attached to // Metadata this tag is attached to
val mangaId: Long, val mangaId: Long,
// Tag namespace // Tag namespace
val namespace: String?, val namespace: String?,
// Tag name // Tag name
val name: String, val name: String,
// Tag type // Tag type
val type: Int val type: Int
) )

View File

@ -2,14 +2,14 @@ package exh.metadata.sql.models
data class SearchTitle( data class SearchTitle(
// Title identifier, unique // Title identifier, unique
val id: Long?, val id: Long?,
// Metadata this title is attached to // Metadata this title is attached to
val mangaId: Long, val mangaId: Long,
// Title // Title
val title: String, val title: String,
// Title type, useful for distinguishing between main/alt titles // Title type, useful for distinguishing between main/alt titles
val type: Int val type: Int
) )

View File

@ -3,7 +3,6 @@ package exh.metadata.sql.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import exh.metadata.sql.models.SearchMetadata import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider {
.table(SearchMetadataTable.TABLE) .table(SearchMetadataTable.TABLE)
.build()) .build())
.prepare() .prepare()
} }

View File

@ -44,4 +44,4 @@ interface SearchTagQueries : DbProvider {
} }
} }
} }
} }

View File

@ -4,8 +4,6 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.inTransaction import eu.kanade.tachiyomi.data.database.inTransaction
import eu.kanade.tachiyomi.data.database.models.Manga
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTitle import exh.metadata.sql.models.SearchTitle
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
@ -46,4 +44,4 @@ interface SearchTitleQueries : DbProvider {
} }
} }
} }
} }

View File

@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId -> val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
if(!response.isSuccessful) { if (!response.isSuccessful) {
response.interceptAsHtml { doc -> response.interceptAsHtml { doc ->
// Find captcha // Find captcha
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) { if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
@ -20,4 +20,4 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
} }
} }
} else response } else response
} }

View File

@ -1,4 +1,6 @@
package exh.search package exh.search
class Namespace(var namespace: String, class Namespace(
var tag: Text? = null) : QueryComponent() var namespace: String,
var tag: Text? = null
) : QueryComponent()

View File

@ -3,4 +3,4 @@ package exh.search
open class QueryComponent { open class QueryComponent {
var excluded = false var excluded = false
var exact = false var exact = false
} }

View File

@ -7,8 +7,10 @@ import exh.metadata.sql.tables.SearchTitleTable
class SearchEngine { class SearchEngine {
private val queryCache = mutableMapOf<String, List<QueryComponent>>() private val queryCache = mutableMapOf<String, List<QueryComponent>>()
fun textToSubQueries(namespace: String?, fun textToSubQueries(
component: Text?): Pair<String, List<String>>? { namespace: String?,
component: Text?
): Pair<String, List<String>>? {
val maybeLenientComponent = component?.let { val maybeLenientComponent = component?.let {
if (!it.exact) if (!it.exact)
it.asLenientTagQueries() it.asLenientTagQueries()
@ -22,20 +24,20 @@ class SearchEngine {
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?" "${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params }.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
} }
return if(namespace != null) { return if (namespace != null) {
var query = """ var query = """
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} (SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
AND ${SearchTagTable.COL_NAMESPACE} LIKE ? AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
""".trimIndent() """.trimIndent()
val params = mutableListOf(escapeLike(namespace)) val params = mutableListOf(escapeLike(namespace))
if(componentTagQuery != null) { if (componentTagQuery != null) {
query += "\n AND ${componentTagQuery.first}" query += "\n AND ${componentTagQuery.first}"
params += componentTagQuery.second params += componentTagQuery.second
} }
"$query)" to params "$query)" to params
} else if(component != null) { } else if (component != null) {
// Match title + tags // Match title + tags
val tagQuery = """ val tagQuery = """
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
@ -59,27 +61,27 @@ class SearchEngine {
val include = mutableListOf<Pair<String, List<String>>>() val include = mutableListOf<Pair<String, List<String>>>()
val exclude = mutableListOf<Pair<String, List<String>>>() val exclude = mutableListOf<Pair<String, List<String>>>()
for(component in q) { for (component in q) {
val query = if(component is Text) { val query = if (component is Text) {
textToSubQueries(null, component) textToSubQueries(null, component)
} else if(component is Namespace) { } else if (component is Namespace) {
if(component.namespace == "uploader") { if (component.namespace == "uploader") {
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?" wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
whereParams += component.tag!!.rawTextEscapedForLike() whereParams += component.tag!!.rawTextEscapedForLike()
null null
} else { } else {
if(component.tag!!.components.size > 0) { if (component.tag!!.components.size > 0) {
//Match namespace + tags // Match namespace + tags
textToSubQueries(component.namespace, component.tag) textToSubQueries(component.namespace, component.tag)
} else { } else {
//Perform namespace search // Perform namespace search
textToSubQueries(component.namespace, null) textToSubQueries(component.namespace, null)
} }
} }
} else error("Unknown query component!") } else error("Unknown query component!")
if(query != null) { if (query != null) {
(if(component.excluded) exclude else include) += query (if (component.excluded) exclude else include) += query
} }
} }
@ -97,14 +99,13 @@ class SearchEngine {
completeParams += pair.second completeParams += pair.second
} }
exclude.forEach { exclude.forEach {
wheres += """ wheres += """
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first}) (meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
""".trimIndent() """.trimIndent()
whereParams += it.second whereParams += it.second
} }
if(wheres.isNotEmpty()) { if (wheres.isNotEmpty()) {
completeParams += whereParams completeParams += whereParams
baseQuery += "\nWHERE\n" baseQuery += "\nWHERE\n"
baseQuery += wheres.joinToString("\nAND\n") baseQuery += wheres.joinToString("\nAND\n")
@ -126,7 +127,7 @@ class SearchEngine {
var nextIsExact = false var nextIsExact = false
fun flushText() { fun flushText() {
if(queuedRawText.isNotEmpty()) { if (queuedRawText.isNotEmpty()) {
queuedText += StringTextComponent(queuedRawText.toString()) queuedText += StringTextComponent(queuedRawText.toString())
queuedRawText.setLength(0) queuedRawText.setLength(0)
} }
@ -150,24 +151,24 @@ class SearchEngine {
} }
} }
for(char in query.toLowerCase()) { for (char in query.toLowerCase()) {
if(char == '"') { if (char == '"') {
inQuotes = !inQuotes inQuotes = !inQuotes
} else if(enableWildcard && (char == '?' || char == '_')) { } else if (enableWildcard && (char == '?' || char == '_')) {
flushText() flushText()
queuedText.add(SingleWildcard(char.toString())) queuedText.add(SingleWildcard(char.toString()))
} else if(enableWildcard && (char == '*' || char == '%')) { } else if (enableWildcard && (char == '*' || char == '%')) {
flushText() flushText()
queuedText.add(MultiWildcard(char.toString())) queuedText.add(MultiWildcard(char.toString()))
} else if(char == '-') { } else if (char == '-') {
nextIsExcluded = true nextIsExcluded = true
} else if(char == '$') { } else if (char == '$') {
nextIsExact = true nextIsExact = true
} else if(char == ':') { } else if (char == ':') {
flushText() flushText()
var flushed = flushToText().rawTextOnly() var flushed = flushToText().rawTextOnly()
//Map tag aliases // Map tag aliases
flushed = when(flushed) { flushed = when (flushed) {
"a" -> "artist" "a" -> "artist"
"c", "char" -> "character" "c", "char" -> "character"
"f" -> "female" "f" -> "female"
@ -179,7 +180,7 @@ class SearchEngine {
else -> flushed else -> flushed
} }
namespace = Namespace(flushed, null) namespace = Namespace(flushed, null)
} else if(char == ' ' && !inQuotes) { } else if (char == ' ' && !inQuotes) {
flushAll() flushAll()
} else { } else {
queuedRawText.append(char) queuedRawText.append(char)
@ -197,7 +198,6 @@ class SearchEngine {
return string.replace("\\", "\\\\") return string.replace("\\", "\\\\")
.replace("_", "\\_") .replace("_", "\\_")
.replace("%", "\\%") .replace("%", "\\%")
} }
} }
} }

View File

@ -3,7 +3,7 @@ package exh.search
import exh.plusAssign import exh.plusAssign
import exh.search.SearchEngine.Companion.escapeLike import exh.search.SearchEngine.Companion.escapeLike
class Text: QueryComponent() { class Text : QueryComponent() {
val components = mutableListOf<TextComponent>() val components = mutableListOf<TextComponent>()
private var query: String? = null private var query: String? = null
@ -12,26 +12,26 @@ class Text: QueryComponent() {
private var rawText: String? = null private var rawText: String? = null
fun asQuery(): String { fun asQuery(): String {
if(query == null) { if (query == null) {
query = rBaseBuilder().toString() query = rBaseBuilder().toString()
} }
return query!! return query!!
} }
fun asLenientTitleQuery(): String { fun asLenientTitleQuery(): String {
if(lenientTitleQuery == null) { if (lenientTitleQuery == null) {
lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString() lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString()
} }
return lenientTitleQuery!! return lenientTitleQuery!!
} }
fun asLenientTagQueries(): List<String> { fun asLenientTagQueries(): List<String> {
if(lenientTagQueries == null) { if (lenientTagQueries == null) {
lenientTagQueries = listOf( lenientTagQueries = listOf(
//Match beginning of tag // Match beginning of tag
rBaseBuilder().append("%").toString(), rBaseBuilder().append("%").toString(),
//Tag word matcher (that matches multiple words) // Tag word matcher (that matches multiple words)
//Can't make it match a single word in Realm :( // Can't make it match a single word in Realm :(
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(), StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
StringBuilder(" ").append(rBaseBuilder()).toString(), StringBuilder(" ").append(rBaseBuilder()).toString(),
rBaseBuilder().append(" ").toString() rBaseBuilder().append(" ").toString()
@ -42,8 +42,8 @@ class Text: QueryComponent() {
fun rBaseBuilder(): StringBuilder { fun rBaseBuilder(): StringBuilder {
val builder = StringBuilder() val builder = StringBuilder()
for(component in components) { for (component in components) {
when(component) { when (component) {
is StringTextComponent -> builder += escapeLike(component.value) is StringTextComponent -> builder += escapeLike(component.value)
is SingleWildcard -> builder += "_" is SingleWildcard -> builder += "_"
is MultiWildcard -> builder += "%" is MultiWildcard -> builder += "%"
@ -52,7 +52,7 @@ class Text: QueryComponent() {
return builder return builder
} }
fun rawTextOnly() = if(rawText != null) fun rawTextOnly() = if (rawText != null)
rawText!! rawText!!
else { else {
rawText = components rawText = components

View File

@ -8,13 +8,19 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.ui.smartsearch.SmartSearchPresenter import exh.ui.smartsearch.SmartSearchPresenter
import exh.util.await import exh.util.await
import info.debatty.java.stringsimilarity.NormalizedLevenshtein import info.debatty.java.stringsimilarity.NormalizedLevenshtein
import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
class SmartSearchEngine(parentContext: CoroutineContext, class SmartSearchEngine(
val extraSearchParams: String? = null): CoroutineScope { parentContext: CoroutineContext,
val extraSearchParams: String? = null
) : CoroutineScope {
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
@ -29,7 +35,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val eligibleManga = supervisorScope { val eligibleManga = supervisorScope {
queries.map { query -> queries.map { query ->
async(Dispatchers.Default) { async(Dispatchers.Default) {
val builtQuery = if(extraSearchParams != null) { val builtQuery = if (extraSearchParams != null) {
"$query ${extraSearchParams.trim()}" "$query ${extraSearchParams.trim()}"
} else query } else query
@ -51,7 +57,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? { suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
val eligibleManga = supervisorScope { val eligibleManga = supervisorScope {
val searchQuery = if(extraSearchParams != null) { val searchQuery = if (extraSearchParams != null) {
"$title ${extraSearchParams.trim()}" "$title ${extraSearchParams.trim()}"
} else title } else title
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io()) val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
@ -71,7 +77,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val splitCleanedTitle = cleanedTitle.split(" ") val splitCleanedTitle = cleanedTitle.split(" ")
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length } val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
if(splitCleanedTitle.isEmpty()) { if (splitCleanedTitle.isEmpty()) {
return emptyList() return emptyList()
} }
@ -99,7 +105,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
// Remove text in brackets // Remove text in brackets
var cleanedTitle = removeTextInBrackets(preTitle, true) var cleanedTitle = removeTextInBrackets(preTitle, true)
if(cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards if (cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
cleanedTitle = removeTextInBrackets(preTitle, false) cleanedTitle = removeTextInBrackets(preTitle, false)
} }
@ -127,7 +133,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
}.toMap() }.toMap()
// Reverse pairs if reading backwards // Reverse pairs if reading backwards
if(!readForward) { if (!readForward) {
val tmp = openingBracketPairs val tmp = openingBracketPairs
openingBracketPairs = closingBracketPairs openingBracketPairs = closingBracketPairs
closingBracketPairs = tmp closingBracketPairs = tmp
@ -136,16 +142,16 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val depthPairs = bracketPairs.map { 0 }.toMutableList() val depthPairs = bracketPairs.map { 0 }.toMutableList()
val result = StringBuilder() val result = StringBuilder()
for(c in if(readForward) text else text.reversed()) { for (c in if (readForward) text else text.reversed()) {
val openingBracketDepthIndex = openingBracketPairs[c] val openingBracketDepthIndex = openingBracketPairs[c]
if(openingBracketDepthIndex != null) { if (openingBracketDepthIndex != null) {
depthPairs[openingBracketDepthIndex]++ depthPairs[openingBracketDepthIndex]++
} else { } else {
val closingBracketDepthIndex = closingBracketPairs[c] val closingBracketDepthIndex = closingBracketPairs[c]
if(closingBracketDepthIndex != null) { if (closingBracketDepthIndex != null) {
depthPairs[closingBracketDepthIndex]-- depthPairs[closingBracketDepthIndex]--
} else { } else {
if(depthPairs.all { it <= 0 }) { if (depthPairs.all { it <= 0 }) {
result.append(c) result.append(c)
} else { } else {
// In brackets, do not append to result // In brackets, do not append to result
@ -183,4 +189,4 @@ class SmartSearchEngine(parentContext: CoroutineContext,
private val titleRegex = Regex("[^a-zA-Z0-9- ]") private val titleRegex = Regex("[^a-zA-Z0-9- ]")
private val consecutiveSpacesRegex = Regex(" +") private val consecutiveSpacesRegex = Regex(" +")
} }
} }

View File

@ -1,29 +1,32 @@
package exh.source package exh.source
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import java.lang.RuntimeException
abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() { abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
/** /**
* Returns the request for the popular manga given the page. * Returns the request for the popular manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int) override fun popularMangaRequest(page: Int) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun popularMangaParse(response: Response) override fun popularMangaParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
@ -32,64 +35,64 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun searchMangaParse(response: Response) override fun searchMangaParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int) override fun latestUpdatesRequest(page: Int) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun latestUpdatesParse(response: Response) override fun latestUpdatesParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response) override fun mangaDetailsParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun chapterListParse(response: Response) override fun chapterListParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response) override fun pageListParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response) override fun imageUrlParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
@ -236,8 +239,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
override fun getFilterList() = delegate.getFilterList() override fun getFilterList() = delegate.getFilterList()
private fun ensureDelegateCompatible() { private fun ensureDelegateCompatible() {
if(versionId != delegate.versionId 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})!") throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
} }
} }
@ -247,4 +250,4 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
init { init {
delegate.bindDelegate(this) delegate.bindDelegate(this)
} }
} }

View File

@ -2,13 +2,19 @@ package exh.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class EnhancedHttpSource(val originalSource: HttpSource, class EnhancedHttpSource(
val enchancedSource: HttpSource): HttpSource() { val originalSource: HttpSource,
val enchancedSource: HttpSource
) : HttpSource() {
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
/** /**
@ -16,16 +22,16 @@ class EnhancedHttpSource(val originalSource: HttpSource,
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int) override fun popularMangaRequest(page: Int) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun popularMangaParse(response: Response) override fun popularMangaParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
@ -34,64 +40,64 @@ class EnhancedHttpSource(val originalSource: HttpSource,
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun searchMangaParse(response: Response) override fun searchMangaParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int) override fun latestUpdatesRequest(page: Int) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun latestUpdatesParse(response: Response) override fun latestUpdatesParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response) override fun mangaDetailsParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun chapterListParse(response: Response) override fun chapterListParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response) override fun pageListParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response) override fun imageUrlParse(response: Response) =
= throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
@ -146,8 +152,8 @@ class EnhancedHttpSource(val originalSource: HttpSource,
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
= source().fetchSearchManga(page, query, filters) source().fetchSearchManga(page, query, filters)
/** /**
* Returns an observable containing a page with a list of latest manga updates. * Returns an observable containing a page with a list of latest manga updates.
@ -202,8 +208,8 @@ class EnhancedHttpSource(val originalSource: HttpSource,
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
override fun prepareNewChapter(chapter: SChapter, manga: SManga) override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
= source().prepareNewChapter(chapter, manga) source().prepareNewChapter(chapter, manga)
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.
@ -211,10 +217,10 @@ class EnhancedHttpSource(val originalSource: HttpSource,
override fun getFilterList() = source().getFilterList() override fun getFilterList() = source().getFilterList()
private fun source(): HttpSource { private fun source(): HttpSource {
return if(prefs.eh_delegateSources().getOrDefault()) { return if (prefs.eh_delegateSources().getOrDefault()) {
enchancedSource enchancedSource
} else { } else {
originalSource originalSource
} }
} }
} }

View File

@ -7,14 +7,14 @@ import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import timber.log.Timber
import kotlin.concurrent.thread import kotlin.concurrent.thread
import timber.log.Timber
class ConfiguringDialogController : DialogController() { class ConfiguringDialogController : DialogController() {
private var materialDialog: MaterialDialog? = null private var materialDialog: MaterialDialog? = null
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
if(savedViewState == null) if (savedViewState == null)
thread { thread {
try { try {
EHConfigurator().configureAll() EHConfigurator().configureAll()
@ -62,4 +62,3 @@ class ConfiguringDialogController : DialogController() {
router.popController(this) router.popController(this)
} }
} }

View File

@ -24,11 +24,13 @@ class EHConfigurator {
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder() private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
.addHeader("Cookie", cookiesHeader(sp)) .addHeader("Cookie", cookiesHeader(sp))
private fun EHentai.execProfileActions(action: String, private fun EHentai.execProfileActions(
name: String, action: String,
set: String, name: String,
sp: Int) set: String,
= configuratorClient.newCall(requestWithCreds(sp) sp: Int
) =
configuratorClient.newCall(requestWithCreds(sp)
.url(uconfigUrl) .url(uconfigUrl)
.post(FormBody.Builder() .post(FormBody.Builder()
.add("profile_action", action) .add("profile_action", action)
@ -44,7 +46,7 @@ class EHConfigurator {
val ehSource = sources.get(EH_SOURCE_ID) as EHentai val ehSource = sources.get(EH_SOURCE_ID) as EHentai
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
//Get hath perks // Get hath perks
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds() val perksPage = configuratorClient.newCall(ehSource.requestWithCreds()
.url(HATH_PERKS_URL) .url(HATH_PERKS_URL)
.build()) .build())
@ -56,13 +58,13 @@ class EHConfigurator {
val name = it.child(0).text().toLowerCase() val name = it.child(0).text().toLowerCase()
val purchased = it.child(2).getElementsByTag("form").isEmpty() val purchased = it.child(2).getElementsByTag("form").isEmpty()
when(name) { when (name) {
//Thumbnail rows // Thumbnail rows
"more thumbs" -> hathPerks.moreThumbs = purchased "more thumbs" -> hathPerks.moreThumbs = purchased
"thumbs up" -> hathPerks.thumbsUp = purchased "thumbs up" -> hathPerks.thumbsUp = purchased
"all thumbs" -> hathPerks.allThumbs = purchased "all thumbs" -> hathPerks.allThumbs = purchased
//Pagination sizing // Pagination sizing
"paging enlargement i" -> hathPerks.pagingEnlargementI = purchased "paging enlargement i" -> hathPerks.pagingEnlargementI = purchased
"paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased "paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased
"paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased "paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased
@ -76,45 +78,45 @@ class EHConfigurator {
} }
fun configure(source: EHentai, hathPerks: EHHathPerksResponse) { fun configure(source: EHentai, hathPerks: EHHathPerksResponse) {
//Delete old app profiles // Delete old app profiles
val scanReq = source.requestWithCreds().url(source.uconfigUrl).build() val scanReq = source.requestWithCreds().url(source.uconfigUrl).build()
val resp = configuratorClient.newCall(scanReq).execute().asJsoup() val resp = configuratorClient.newCall(scanReq).execute().asJsoup()
var lastDoc = resp var lastDoc = resp
resp.select(PROFILE_SELECTOR).forEach { resp.select(PROFILE_SELECTOR).forEach {
if(it.text() == PROFILE_NAME) { if (it.text() == PROFILE_NAME) {
val id = it.attr("value") val id = it.attr("value")
//Delete old profile // Delete old profile
lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup() lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup()
} }
} }
//Find available profile slot // Find available profile slot
val availableProfiles = (1 .. 3).toMutableList() val availableProfiles = (1..3).toMutableList()
lastDoc.select(PROFILE_SELECTOR).forEach { lastDoc.select(PROFILE_SELECTOR).forEach {
availableProfiles.remove(it.attr("value").toInt()) availableProfiles.remove(it.attr("value").toInt())
} }
//No profile slots left :( // 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!") throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
//Create profile in available slot // Create profile in available slot
val slot = availableProfiles.first() val slot = availableProfiles.first()
val response = source.execProfileActions("create", val response = source.execProfileActions("create",
PROFILE_NAME, PROFILE_NAME,
slot.toString(), slot.toString(),
1) 1)
//Build new profile // Build new profile
val form = EhUConfigBuilder().build(hathPerks) val form = EhUConfigBuilder().build(hathPerks)
//Send new profile to server // Send new profile to server
configuratorClient.newCall(source.requestWithCreds(sp = slot) configuratorClient.newCall(source.requestWithCreds(sp = slot)
.url(source.uconfigUrl) .url(source.uconfigUrl)
.post(form) .post(form)
.build()).execute() .build()).execute()
//Persist slot + sk // Persist slot + sk
source.spPref().set(slot) source.spPref().set(slot)
val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find { val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find {
@ -127,19 +129,19 @@ class EHConfigurator {
it.startsWith("hath_perks=") it.startsWith("hath_perks=")
}?.removePrefix("hath_perks=")?.substringBefore(';') }?.removePrefix("hath_perks=")?.substringBefore(';')
if(keyCookie != null) if (keyCookie != null)
prefs.eh_settingsKey().set(keyCookie) prefs.eh_settingsKey().set(keyCookie)
if(sessionCookie != null) if (sessionCookie != null)
prefs.eh_sessionCookie().set(sessionCookie) prefs.eh_sessionCookie().set(sessionCookie)
if(hathPerksCookie != null) if (hathPerksCookie != null)
prefs.eh_hathPerksCookies().set(hathPerksCookie) prefs.eh_hathPerksCookies().set(hathPerksCookie)
} }
companion object { companion object {
private const val PROFILE_NAME = "TachiyomiEH App" private const val PROFILE_NAME = "TachiyomiEH App"
private const val UCONFIG_URL = "/uconfig.php" private const val UCONFIG_URL = "/uconfig.php"
//Always use E-H here as EXH does not have a perks page // Always use E-H here as EXH does not have a perks page
private const val HATH_PERKS_URL = "https://e-hentai.org/hathperks.php" private const val HATH_PERKS_URL = "https://e-hentai.org/hathperks.php"
private const val PROFILE_SELECTOR = "[name=profile_set] > option" private const val PROFILE_SELECTOR = "[name=profile_set] > option"
} }
} }

View File

@ -9,7 +9,6 @@ class EHHathPerksResponse {
var pagingEnlargementII = false var pagingEnlargementII = false
var pagingEnlargementIII = false var pagingEnlargementIII = false
override fun toString() override fun toString() =
= "EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)" "EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
} }

View File

@ -11,7 +11,7 @@ class EhUConfigBuilder {
fun build(hathPerks: EHHathPerksResponse): FormBody { fun build(hathPerks: EHHathPerksResponse): FormBody {
val configItems = mutableListOf<ConfigItem>() val configItems = mutableListOf<ConfigItem>()
configItems += when(prefs.imageQuality() configItems += when (prefs.imageQuality()
.getOrDefault() .getOrDefault()
.toLowerCase()) { .toLowerCase()) {
"ovrs_2400" -> Entry.ImageSize.`2400` "ovrs_2400" -> Entry.ImageSize.`2400`
@ -23,17 +23,17 @@ class EhUConfigBuilder {
else -> Entry.ImageSize.AUTO else -> Entry.ImageSize.AUTO
} }
configItems += if(prefs.useHentaiAtHome().getOrDefault()) configItems += if (prefs.useHentaiAtHome().getOrDefault())
Entry.UseHentaiAtHome.YES Entry.UseHentaiAtHome.YES
else else
Entry.UseHentaiAtHome.NO Entry.UseHentaiAtHome.NO
configItems += if(prefs.useJapaneseTitle().getOrDefault()) configItems += if (prefs.useJapaneseTitle().getOrDefault())
Entry.TitleDisplayLanguage.JAPANESE Entry.TitleDisplayLanguage.JAPANESE
else else
Entry.TitleDisplayLanguage.DEFAULT Entry.TitleDisplayLanguage.DEFAULT
configItems += if(prefs.eh_useOriginalImages().getOrDefault()) configItems += if (prefs.eh_useOriginalImages().getOrDefault())
Entry.UseOriginalImages.YES Entry.UseOriginalImages.YES
else else
Entry.UseOriginalImages.NO Entry.UseOriginalImages.NO
@ -56,7 +56,7 @@ class EhUConfigBuilder {
configItems += Entry.UseMPV() configItems += Entry.UseMPV()
configItems += Entry.ShowPopularRightNowPane() configItems += Entry.ShowPopularRightNowPane()
//Actually build form body // Actually build form body
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
configItems.forEach { configItems.forEach {
formBody.add(it.key, it.value) formBody.add(it.key, it.value)
@ -67,14 +67,14 @@ class EhUConfigBuilder {
} }
object Entry { object Entry {
enum class UseHentaiAtHome(override val value: String): ConfigItem { enum class UseHentaiAtHome(override val value: String) : ConfigItem {
YES("0"), YES("0"),
NO("1"); NO("1");
override val key = "uh" override val key = "uh"
} }
enum class ImageSize(override val value: String): ConfigItem { enum class ImageSize(override val value: String) : ConfigItem {
AUTO("0"), AUTO("0"),
`2400`("5"), `2400`("5"),
`1600`("4"), `1600`("4"),
@ -85,20 +85,20 @@ object Entry {
override val key = "xr" override val key = "xr"
} }
enum class TitleDisplayLanguage(override val value: String): ConfigItem { enum class TitleDisplayLanguage(override val value: String) : ConfigItem {
DEFAULT("0"), DEFAULT("0"),
JAPANESE("1"); JAPANESE("1");
override val key = "tl" override val key = "tl"
} }
//Locked to extended mode as that's what the parser and toplists use // Locked to extended mode as that's what the parser and toplists use
class DisplayMode: ConfigItem { class DisplayMode : ConfigItem {
override val key = "dm" override val key = "dm"
override val value = "2" override val value = "2"
} }
enum class SearchResultsCount(override val value: String): ConfigItem { enum class SearchResultsCount(override val value: String) : ConfigItem {
`25`("0"), `25`("0"),
`50`("1"), `50`("1"),
`100`("2"), `100`("2"),
@ -107,7 +107,7 @@ object Entry {
override val key = "rc" override val key = "rc"
} }
enum class ThumbnailRows(override val value: String): ConfigItem { enum class ThumbnailRows(override val value: String) : ConfigItem {
`4`("0"), `4`("0"),
`10`("1"), `10`("1"),
`20`("2"), `20`("2"),
@ -116,21 +116,21 @@ object Entry {
override val key = "tr" override val key = "tr"
} }
enum class UseOriginalImages(override val value: String): ConfigItem { enum class UseOriginalImages(override val value: String) : ConfigItem {
NO("0"), NO("0"),
YES("1"); YES("1");
override val key = "oi" override val key = "oi"
} }
//Locked to no MPV as that's what the parser uses // Locked to no MPV as that's what the parser uses
class UseMPV: ConfigItem { class UseMPV : ConfigItem {
override val key = "qb" override val key = "qb"
override val value = "0" override val value = "0"
} }
//Locked to no popular pane as we can't parse it // Locked to no popular pane as we can't parse it
class ShowPopularRightNowPane: ConfigItem { class ShowPopularRightNowPane : ConfigItem {
override val key = "pp" override val key = "pp"
override val value = "1" override val value = "1"
} }
@ -139,4 +139,4 @@ object Entry {
interface ConfigItem { interface ConfigItem {
val key: String val key: String
val value: String val value: String
} }

View File

@ -32,10 +32,10 @@ class WarnConfigureDialogController : DialogController() {
companion object { companion object {
fun uploadSettings(router: Router) { fun uploadSettings(router: Router) {
if(Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault()) if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
WarnConfigureDialogController().showDialog(router) WarnConfigureDialogController().showDialog(router)
else else
ConfiguringDialogController().showDialog(router) ConfiguringDialogController().showDialog(router)
} }
} }
} }

View File

@ -1,18 +1,18 @@
package exh.ui package exh.ui
import java.util.UUID
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
typealias LoadingHandle = String typealias LoadingHandle = String
/** /**
* Class used to manage loader UIs * Class used to manage loader UIs
*/ */
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope { class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
override val coroutineContext = Dispatchers.Main + parentContext override val coroutineContext = Dispatchers.Main + parentContext
private val openLoadingHandles = mutableListOf<LoadingHandle>() private val openLoadingHandles = mutableListOf<LoadingHandle>()
@ -25,7 +25,7 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
handle to (openLoadingHandles.size == 1) handle to (openLoadingHandles.size == 1)
} }
if(shouldUpdateLoadingStatus) { if (shouldUpdateLoadingStatus) {
launch { launch {
updateLoadingStatus(true) updateLoadingStatus(true)
} }
@ -36,13 +36,13 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
@Synchronized @Synchronized
fun closeProgressBar(handle: LoadingHandle?) { fun closeProgressBar(handle: LoadingHandle?) {
if(handle == null) return if (handle == null) return
val shouldUpdateLoadingStatus = synchronized(this) { val shouldUpdateLoadingStatus = synchronized(this) {
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty() openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
} }
if(shouldUpdateLoadingStatus) { if (shouldUpdateLoadingStatus) {
launch { launch {
updateLoadingStatus(false) updateLoadingStatus(false)
} }

View File

@ -6,11 +6,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope { abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
abstract val layoutId: Int abstract val layoutId: Int

View File

@ -1,6 +1,5 @@
package exh.ui.batchadd package exh.ui.batchadd
import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -44,14 +43,14 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
progressSubscriptions.clear() progressSubscriptions.clear()
if(it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) { if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
showProgress(this) showProgress(this)
progressSubscriptions += presenter.progressRelay progressSubscriptions += presenter.progressRelay
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.combineLatest(presenter.progressTotalRelay, { progress, total -> .combineLatest(presenter.progressTotalRelay, { progress, total ->
//Show hide dismiss button // Show hide dismiss button
progress_dismiss_btn.visibility = progress_dismiss_btn.visibility =
if(progress == total) if (progress == total)
View.VISIBLE View.VISIBLE
else View.GONE else View.GONE
@ -79,7 +78,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
}?.let { }?.let {
progressSubscriptions += it progressSubscriptions += it
} }
} else if(it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) { } else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
hideProgress(this) hideProgress(this)
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE) presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
} }
@ -124,8 +123,8 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
private fun formatProgress(progress: Int, total: Int) = "$progress/$total" private fun formatProgress(progress: Int, total: Int) = "$progress/$total"
private fun addGalleries(galleries: String) { private fun addGalleries(galleries: String) {
//Check text box has content // Check text box has content
if(galleries.isBlank()) { if (galleries.isBlank()) {
noGalleriesSpecified() noGalleriesSpecified()
return return
} }

View File

@ -8,7 +8,7 @@ import exh.GalleryAdder
import exh.metadata.nullIfBlank import exh.metadata.nullIfBlank
import kotlin.concurrent.thread import kotlin.concurrent.thread
class BatchAddPresenter: BasePresenter<BatchAddController>() { class BatchAddPresenter : BasePresenter<BatchAddController>() {
private val galleryAdder by lazy { GalleryAdder() } private val galleryAdder by lazy { GalleryAdder() }
@ -34,7 +34,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
splitGalleries.forEachIndexed { i, s -> splitGalleries.forEachIndexed { i, s ->
val result = galleryAdder.addGallery(s, true) val result = galleryAdder.addGallery(s, true)
if(result is GalleryAddEvent.Success) { if (result is GalleryAddEvent.Success) {
succeeded.add(s) succeeded.add(s)
} else { } else {
failed.add(s) failed.add(s)
@ -46,7 +46,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
}) + " " + result.logMessage) }) + " " + result.logMessage)
} }
//Show report // Show report
val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)" val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)"
eventRelay?.call(summary) eventRelay?.call(summary)
} }

View File

@ -7,21 +7,23 @@ import android.webkit.WebView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.util.system.asJsoup import eu.kanade.tachiyomi.util.system.asJsoup
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
import java.nio.charset.Charset
import org.jsoup.nodes.DataNode import org.jsoup.nodes.DataNode
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.nio.charset.Charset
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AutoSolvingWebViewClient(activity: BrowserActionActivity, class AutoSolvingWebViewClient(
verifyComplete: (String) -> Boolean, activity: BrowserActionActivity,
injectScript: String?, verifyComplete: (String) -> Boolean,
headers: Map<String, String>) injectScript: String?,
: HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) { headers: Map<String, String>
) :
HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Inject our custom script into the recaptcha iframes // Inject our custom script into the recaptcha iframes
val lastPathSegment = request.url.pathSegments.lastOrNull() val lastPathSegment = request.url.pathSegments.lastOrNull()
if(lastPathSegment == "anchor" || lastPathSegment == "bframe") { if (lastPathSegment == "anchor" || lastPathSegment == "bframe") {
val oReq = request.toOkHttpRequest() val oReq = request.toOkHttpRequest()
val response = activity.httpClient.newCall(oReq).execute() val response = activity.httpClient.newCall(oReq).execute()
val doc = response.asJsoup() val doc = response.asJsoup()
@ -34,4 +36,4 @@ class AutoSolvingWebViewClient(activity: BrowserActionActivity,
} }
return super.shouldInterceptRequest(view, request) return super.shouldInterceptRequest(view, request)
} }
} }

View File

@ -4,17 +4,19 @@ import android.os.Build
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
open class BasicWebViewClient(protected val activity: BrowserActionActivity, open class BasicWebViewClient(
protected val verifyComplete: (String) -> Boolean, protected val activity: BrowserActionActivity,
private val injectScript: String?) : WebViewClient() { protected val verifyComplete: (String) -> Boolean,
private val injectScript: String?
) : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
if(verifyComplete(url)) { if (verifyComplete(url)) {
activity.finish() activity.finish()
} else { } else {
if(injectScript != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) if (injectScript != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
view.evaluateJavascript("(function() {$injectScript})();", null) view.evaluateJavascript("(function() {$injectScript})();", null)
} }
} }
} }

View File

@ -6,7 +6,12 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.view.MotionEvent import android.view.MotionEvent
import android.webkit.* import android.webkit.CookieManager
import android.webkit.CookieSyncManager
import android.webkit.JavascriptInterface
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -22,6 +27,9 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.util.melt import exh.util.melt
import java.io.Serializable
import java.net.URL
import java.util.UUID
import kotlinx.android.synthetic.main.eh_activity_captcha.* import kotlinx.android.synthetic.main.eh_activity_captcha.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -33,10 +41,6 @@ import rx.Single
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.Serializable
import java.net.URL
import java.util.*
import kotlin.collections.HashMap
class BrowserActionActivity : AppCompatActivity() { class BrowserActionActivity : AppCompatActivity() {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
@ -58,8 +62,8 @@ class BrowserActionActivity : AppCompatActivity() {
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha) setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1) val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
val source = if(originalSource != null) { val source = if (originalSource != null) {
originalSource as? ActionCompletionVerifier originalSource as? ActionCompletionVerifier
?: run { ?: run {
(originalSource as? HttpSource)?.let { (originalSource as? HttpSource)?.let {
@ -72,24 +76,24 @@ class BrowserActionActivity : AppCompatActivity() {
it.value.joinToString(",") 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>? val cookies: HashMap<String, String>? =
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String> intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
val script: String? = intent.getStringExtra(SCRIPT_EXTRA) val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
val url: String? = intent.getStringExtra(URL_EXTRA) val url: String? = intent.getStringExtra(URL_EXTRA)
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA) val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
val verifyComplete = if(source != null) { val verifyComplete = if (source != null) {
source::verifyComplete!! source::verifyComplete!!
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean } else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
if(verifyComplete == null || url == null) { if (verifyComplete == null || url == null) {
finish() finish()
return return
} }
val actionStr = actionName ?: "Solve captcha" val actionStr = actionName ?: "Solve captcha"
toolbar.title = if(source != null) { toolbar.title = if (source != null) {
"${source.name}: $actionStr" "${source.name}: $actionStr"
} else actionStr } else actionStr
@ -115,13 +119,13 @@ class BrowserActionActivity : AppCompatActivity() {
webview.webChromeClient = object : WebChromeClient() { webview.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean { override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
if(message.startsWith("exh-")) { if (message.startsWith("exh-")) {
loadedInners++ loadedInners++
// Wait for both inner scripts to be loaded // Wait for both inner scripts to be loaded
if(loadedInners >= 2) { if (loadedInners >= 2) {
// Attempt to autosolve captcha // Attempt to autosolve captcha
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault() if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault() &&
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webview.post { webview.post {
// 10 seconds to auto-solve captcha // 10 seconds to auto-solve captcha
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10 strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
@ -141,7 +145,7 @@ class BrowserActionActivity : AppCompatActivity() {
} }
webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) { if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
// Fetch auto-solve credentials early for speed // Fetch auto-solve credentials early for speed
credentialsObservable = httpClient.newCall(Request.Builder() credentialsObservable = httpClient.newCall(Request.Builder()
// Rob demo credentials // Rob demo credentials
@ -196,11 +200,11 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@JavascriptInterface @JavascriptInterface
fun callback(result: String?, loopId: String, stage: Int) { fun callback(result: String?, loopId: String, stage: Int) {
if(loopId != currentLoopId) return if (loopId != currentLoopId) return
when(stage) { when (stage) {
STAGE_CHECKBOX -> { STAGE_CHECKBOX -> {
if(result!!.toBoolean()) { if (result!!.toBoolean()) {
webview.postDelayed({ webview.postDelayed({
getAudioButtonLocation(loopId) getAudioButtonLocation(loopId)
}, 250) }, 250)
@ -211,7 +215,7 @@ class BrowserActionActivity : AppCompatActivity() {
} }
} }
STAGE_GET_AUDIO_BTN_LOCATION -> { STAGE_GET_AUDIO_BTN_LOCATION -> {
if(result != null) { if (result != null) {
val splitResult = result.split(" ").map { it.toFloat() } val splitResult = result.split(" ").map { it.toFloat() }
val origX = splitResult[0] val origX = splitResult[0]
val origY = splitResult[1] val origY = splitResult[1]
@ -231,11 +235,11 @@ class BrowserActionActivity : AppCompatActivity() {
} }
} }
STAGE_DOWNLOAD_AUDIO -> { STAGE_DOWNLOAD_AUDIO -> {
if(result != null) { if (result != null) {
Timber.d("Got audio URL: $result") Timber.d("Got audio URL: $result")
performRecognize(result) performRecognize(result)
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe ({ .subscribe({
Timber.d("Got audio transcript: $it") Timber.d("Got audio transcript: $it")
webview.post { webview.post {
typeResult(loopId, it!! typeResult(loopId, it!!
@ -253,7 +257,7 @@ class BrowserActionActivity : AppCompatActivity() {
} }
} }
STAGE_TYPE_RESULT -> { STAGE_TYPE_RESULT -> {
if(result!!.toBoolean()) { if (result!!.toBoolean()) {
// Fail if captcha still not solved after 1.5s // Fail if captcha still not solved after 1.5s
strictValidationStartTime = System.currentTimeMillis() + 1500 strictValidationStartTime = System.currentTimeMillis() + 1500
} else { } else {
@ -293,7 +297,7 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun doStageCheckbox(loopId: String) { fun doStageCheckbox(loopId: String) {
if(loopId != currentLoopId) return if (loopId != currentLoopId) return
webview.evaluateJavascript(""" webview.evaluateJavascript("""
(function() { (function() {
@ -415,27 +419,26 @@ class BrowserActionActivity : AppCompatActivity() {
doStageCheckbox(loopId) doStageCheckbox(loopId)
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@JavascriptInterface @JavascriptInterface
fun validateCaptchaCallback(result: Boolean, loopId: String) { fun validateCaptchaCallback(result: Boolean, loopId: String) {
if(loopId != validateCurrentLoopId) return if (loopId != validateCurrentLoopId) return
if(result) { if (result) {
Timber.d("Captcha solved!") Timber.d("Captcha solved!")
webview.post { webview.post {
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null) webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
} }
val asbtn = intent.getStringExtra(ASBTN_EXTRA) val asbtn = intent.getStringExtra(ASBTN_EXTRA)
if(asbtn != null) { if (asbtn != null) {
webview.post { webview.post {
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null) webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
} }
} }
} else { } else {
val savedStrictValidationStartTime = strictValidationStartTime val savedStrictValidationStartTime = strictValidationStartTime
if(savedStrictValidationStartTime != null if (savedStrictValidationStartTime != null &&
&& System.currentTimeMillis() > savedStrictValidationStartTime) { System.currentTimeMillis() > savedStrictValidationStartTime) {
captchaSolveFail() captchaSolveFail()
} else { } else {
webview.postDelayed({ webview.postDelayed({
@ -447,7 +450,7 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun runValidateCaptcha(loopId: String) { fun runValidateCaptcha(loopId: String) {
if(loopId != validateCurrentLoopId) return if (loopId != validateCurrentLoopId) return
webview.evaluateJavascript(""" webview.evaluateJavascript("""
(function() { (function() {
@ -624,12 +627,14 @@ class BrowserActionActivity : AppCompatActivity() {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
fun launchCaptcha(context: Context, fun launchCaptcha(
source: ActionCompletionVerifier, context: Context,
cookies: Map<String, String>, source: ActionCompletionVerifier,
script: String?, cookies: Map<String, String>,
url: String, script: String?,
autoSolveSubmitBtnSelector: String? = null) { url: String,
autoSolveSubmitBtnSelector: String? = null
) {
val intent = baseIntent(context).apply { val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, source.id) putExtra(SOURCE_ID_EXTRA, source.id)
putExtra(COOKIES_EXTRA, HashMap(cookies)) putExtra(COOKIES_EXTRA, HashMap(cookies))
@ -641,9 +646,11 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
fun launchUniversal(context: Context, fun launchUniversal(
source: HttpSource, context: Context,
url: String) { source: HttpSource,
url: String
) {
val intent = baseIntent(context).apply { val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, source.id) putExtra(SOURCE_ID_EXTRA, source.id)
putExtra(URL_EXTRA, url) putExtra(URL_EXTRA, url)
@ -652,9 +659,11 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
fun launchUniversal(context: Context, fun launchUniversal(
sourceId: Long, context: Context,
url: String) { sourceId: Long,
url: String
) {
val intent = baseIntent(context).apply { val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, sourceId) putExtra(SOURCE_ID_EXTRA, sourceId)
putExtra(URL_EXTRA, url) putExtra(URL_EXTRA, url)
@ -663,11 +672,13 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
fun launchAction(context: Context, fun launchAction(
completionVerifier: ActionCompletionVerifier, context: Context,
script: String?, completionVerifier: ActionCompletionVerifier,
url: String, script: String?,
actionName: String) { url: String,
actionName: String
) {
val intent = baseIntent(context).apply { val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, completionVerifier.id) putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
putExtra(SCRIPT_EXTRA, script) putExtra(SCRIPT_EXTRA, script)
@ -678,12 +689,14 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
fun launchAction(context: Context, fun launchAction(
completionVerifier: (String) -> Boolean, context: Context,
script: String?, completionVerifier: (String) -> Boolean,
url: String, script: String?,
actionName: String, url: String,
headers: Map<String, String>? = emptyMap()) { actionName: String,
headers: Map<String, String>? = emptyMap()
) {
val intent = baseIntent(context).apply { val intent = baseIntent(context).apply {
putExtra(HEADERS_EXTRA, HashMap(headers)) putExtra(HEADERS_EXTRA, HashMap(headers))
putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable) putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable)
@ -697,7 +710,7 @@ class BrowserActionActivity : AppCompatActivity() {
} }
} }
class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHttpSource(source), class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source),
ActionCompletionVerifier { ActionCompletionVerifier {
override val versionId get() = source.versionId override val versionId get() = source.versionId
override val lang: String get() = source.lang override val lang: String get() = source.lang
@ -708,4 +721,3 @@ class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHtt
interface ActionCompletionVerifier : Source { interface ActionCompletionVerifier : Source {
fun verifyComplete(url: String): Boolean fun verifyComplete(url: String): Boolean
} }

View File

@ -7,11 +7,13 @@ import android.webkit.WebView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
open class HeadersInjectingWebViewClient(activity: BrowserActionActivity, open class HeadersInjectingWebViewClient(
verifyComplete: (String) -> Boolean, activity: BrowserActionActivity,
injectScript: String?, verifyComplete: (String) -> Boolean,
private val headers: Map<String, String>) injectScript: String?,
: BasicWebViewClient(activity, verifyComplete, injectScript) { private val headers: Map<String, String>
) :
BasicWebViewClient(activity, verifyComplete, injectScript) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Temp disabled as it's unreliable // Temp disabled as it's unreliable

View File

@ -16,4 +16,4 @@ fun WebResourceRequest.toOkHttpRequest(): Request {
} }
return request.build() return request.build()
} }

View File

@ -23,7 +23,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.eh_activity_intercept) setContentView(R.layout.eh_activity_intercept)
//Show back button // Show back button
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -31,7 +31,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
} }
private fun processLink() { private fun processLink() {
if(Intent.ACTION_VIEW == intent.action) { if (Intent.ACTION_VIEW == intent.action) {
intercept_progress.visible() intercept_progress.visible()
intercept_status.text = "Loading gallery..." intercept_status.text = "Loading gallery..."
presenter.loadGallery(intent.dataString) presenter.loadGallery(intent.dataString)
@ -52,7 +52,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
statusSubscription = presenter.status statusSubscription = presenter.status
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
when(it) { when (it) {
is InterceptResult.Success -> { is InterceptResult.Success -> {
intercept_progress.gone() intercept_progress.gone()
intercept_status.text = "Launching app..." intercept_status.text = "Launching app..."

View File

@ -3,8 +3,8 @@ package exh.ui.intercept
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import exh.GalleryAddEvent import exh.GalleryAddEvent
import exh.GalleryAdder import exh.GalleryAdder
import rx.subjects.BehaviorSubject
import kotlin.concurrent.thread import kotlin.concurrent.thread
import rx.subjects.BehaviorSubject
class InterceptActivityPresenter : BasePresenter<InterceptActivity>() { class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
private val galleryAdder = GalleryAdder() private val galleryAdder = GalleryAdder()
@ -13,11 +13,11 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
@Synchronized @Synchronized
fun loadGallery(gallery: String) { fun loadGallery(gallery: String) {
//Do not load gallery if already loading // Do not load gallery if already loading
if(status.value is InterceptResult.Idle) { if (status.value is InterceptResult.Idle) {
status.onNext(InterceptResult.Loading()) status.onNext(InterceptResult.Loading())
//Load gallery async // Load gallery async
thread { thread {
val result = galleryAdder.addGallery(gallery) val result = galleryAdder.addGallery(gallery)
@ -35,6 +35,6 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
sealed class InterceptResult { sealed class InterceptResult {
class Idle : InterceptResult() class Idle : InterceptResult()
class Loading : InterceptResult() class Loading : InterceptResult()
data class Success(val mangaId: Long): InterceptResult() data class Success(val mangaId: Long) : InterceptResult()
data class Failure(val reason: String): InterceptResult() data class Failure(val reason: String) : InterceptResult()
} }

View File

@ -28,21 +28,21 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
val prefs: PreferencesHelper by injectLazy() val prefs: PreferencesHelper by injectLazy()
val fingerprintSupported val fingerprintSupported
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
&& Reprint.isHardwarePresent() Reprint.isHardwarePresent() &&
&& Reprint.hasFingerprintRegistered() Reprint.hasFingerprintRegistered()
val useFingerprint val useFingerprint
get() = fingerprintSupported get() = fingerprintSupported &&
&& prefs.eh_lockUseFingerprint().getOrDefault() prefs.eh_lockUseFingerprint().getOrDefault()
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onAttached() { override fun onAttached() {
super.onAttached() super.onAttached()
if(fingerprintSupported) { if (fingerprintSupported) {
updateSummary() updateSummary()
onChange { onChange {
if(it as Boolean) if (it as Boolean)
tryChange() tryChange()
else else
prefs.eh_lockUseFingerprint().set(false) prefs.eh_lockUseFingerprint().set(false)
@ -51,7 +51,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
} else { } else {
title = "Fingerprint unsupported" title = "Fingerprint unsupported"
shouldDisableView = true shouldDisableView = true
summary = if(!Reprint.hasFingerprintRegistered()) summary = if (!Reprint.hasFingerprintRegistered())
"No fingerprints enrolled!" "No fingerprints enrolled!"
else else
"Fingerprint unlock is unsupported on this device!" "Fingerprint unlock is unsupported on this device!"
@ -61,7 +61,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
private fun updateSummary() { private fun updateSummary() {
isChecked = useFingerprint isChecked = useFingerprint
title = if(isChecked) title = if (isChecked)
"Fingerprint enabled" "Fingerprint enabled"
else else
"Fingerprint disabled" "Fingerprint disabled"
@ -146,4 +146,4 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
subscription.unsubscribe() subscription.unsubscribe()
} }
} }
} }

View File

@ -1,6 +1,5 @@
package exh.ui.lock package exh.ui.lock
import android.content.Intent
import android.view.WindowManager import android.view.WindowManager
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
@ -8,7 +7,6 @@ import com.bluelinelabs.conductor.RouterTransaction
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date
object LockActivityDelegate { object LockActivityDelegate {
private val preferences by injectLazy<PreferencesHelper>() private val preferences by injectLazy<PreferencesHelper>()
@ -20,7 +18,6 @@ object LockActivityDelegate {
.popChangeHandler(LockChangeHandler(animate))) .popChangeHandler(LockChangeHandler(animate)))
} }
fun onCreate(activity: FragmentActivity) { fun onCreate(activity: FragmentActivity) {
preferences.secureScreen().asObservable() preferences.secureScreen().asObservable()
.subscribe { .subscribe {
@ -42,5 +39,4 @@ object LockActivityDelegate {
private fun isAppLocked(router: Router): Boolean { private fun isAppLocked(router: Router): Boolean {
return router.backstack.lastOrNull()?.controller() is LockController return router.backstack.lastOrNull()?.controller() is LockController
} }
} }

View File

@ -7,10 +7,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import java.util.* import java.util.ArrayList
class LockChangeHandler : AnimatorChangeHandler { class LockChangeHandler : AnimatorChangeHandler {
constructor(): super() constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush) constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
@ -36,6 +36,4 @@ class LockChangeHandler : AnimatorChangeHandler {
override fun copy(): ControllerChangeHandler = override fun copy(): ControllerChangeHandler =
LockChangeHandler(animationDuration, removesFromViewOnPush()) LockChangeHandler(animationDuration, removesFromViewOnPush())
} }

Some files were not shown because too many files have changed in this diff Show More