Optimize imports, disallow wildcard imports because of klint, run linter
This commit is contained in:
parent
f18891a07e
commit
23ac3d18e5
@ -43,7 +43,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
put(COL_FLAGS, obj.flags)
|
||||
val orderString = obj.mangaOrder.joinToString("/")
|
||||
put(COL_MANGA_ORDER, orderString)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,4 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_URL, manga.url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ object CategoryTable {
|
||||
$COL_MANGA_ORDER TEXT NOT NULL
|
||||
)"""
|
||||
|
||||
|
||||
val addMangaOrder: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import kotlin.reflect.KClass
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* LEWD!
|
||||
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
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.JsonObject
|
||||
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.asObservableSuccess
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
@ -36,23 +46,30 @@ import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.ignore
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.*
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
// TODO Consider gallery updating when doing tabbed browsing
|
||||
class EHentai(override val id: Long,
|
||||
val exh: Boolean,
|
||||
val context: Context) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||
class EHentai(
|
||||
override val id: Long,
|
||||
val exh: Boolean,
|
||||
val context: Context
|
||||
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||
override val metaClass = EHentaiSearchMetadata::class
|
||||
|
||||
val schema: String
|
||||
@ -98,10 +115,10 @@ class EHentai(override val id: Long,
|
||||
favElement?.attr("style")?.substring(14, 17)
|
||||
),
|
||||
manga = Manga.create(id).apply {
|
||||
//Get title
|
||||
// Get title
|
||||
title = thumbnailElement.attr("title")
|
||||
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
||||
//Get image
|
||||
// Get image
|
||||
thumbnail_url = thumbnailElement.attr("src")
|
||||
|
||||
// TODO Parse genre + uploader + tags
|
||||
@ -110,9 +127,9 @@ class EHentai(override val id: Long,
|
||||
|
||||
val parsedLocation = doc.location().toHttpUrlOrNull()
|
||||
|
||||
//Add to page if required
|
||||
val hasNextPage = if (parsedLocation == null
|
||||
|| !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||
// Add to page if required
|
||||
val hasNextPage = if (parsedLocation == null ||
|
||||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||
select("a[onclick=return false]").last()?.let {
|
||||
it.text() == ">"
|
||||
} ?: false
|
||||
@ -212,8 +229,11 @@ class EHentai(override val id: Long,
|
||||
}
|
||||
}!!
|
||||
|
||||
private fun fetchChapterPage(chapter: SChapter, np: String,
|
||||
pastUrls: List<String> = emptyList()): Observable<List<String>> {
|
||||
private fun fetchChapterPage(
|
||||
chapter: SChapter,
|
||||
np: String,
|
||||
pastUrls: List<String> = emptyList()
|
||||
): Observable<List<String>> {
|
||||
val urls = ArrayList(pastUrls)
|
||||
return chapterPageCall(np).flatMap {
|
||||
val jsoup = it.asJsoup()
|
||||
@ -245,7 +265,7 @@ class EHentai(override val id: Long,
|
||||
else
|
||||
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) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
@ -377,7 +397,7 @@ class EHentai(override val id: Long,
|
||||
|
||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||
|
||||
//Parse the table
|
||||
// Parse the table
|
||||
select("#gdd tr").forEach {
|
||||
val left = it.select(".gdt1").text().nullIfBlank()?.trim()
|
||||
val rightElement = it.selectFirst(".gdt2")
|
||||
@ -407,13 +427,13 @@ class EHentai(override val id: Long,
|
||||
}
|
||||
|
||||
lastUpdateCheck = System.currentTimeMillis()
|
||||
if (datePosted != null
|
||||
&& lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
|
||||
if (datePosted != null &&
|
||||
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
|
||||
aged = true
|
||||
XLog.d("aged %s - too old", title)
|
||||
}
|
||||
|
||||
//Parse ratings
|
||||
// Parse ratings
|
||||
ignore {
|
||||
averageRating = select("#rating_label")
|
||||
.text()
|
||||
@ -428,7 +448,7 @@ class EHentai(override val id: Long,
|
||||
?.toInt()
|
||||
}
|
||||
|
||||
//Parse tags
|
||||
// Parse tags
|
||||
tags.clear()
|
||||
select("#taglist tr").forEach {
|
||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||
@ -465,7 +485,7 @@ class EHentai(override val id: Long,
|
||||
fun realImageUrlParse(response: Response, page: Page): String {
|
||||
with(response.asJsoup()) {
|
||||
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 {
|
||||
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()
|
||||
val doc = response2.asJsoup()
|
||||
|
||||
//Parse favorites
|
||||
// Parse favorites
|
||||
val parsed = extendedGenericMangaParse(doc)
|
||||
result += parsed.first
|
||||
|
||||
//Parse fav names
|
||||
// Parse fav names
|
||||
if (favNames == null)
|
||||
favNames = doc.select(".fp:not(.fps)").mapNotNull {
|
||||
it.child(2).text()
|
||||
}
|
||||
|
||||
//Next page
|
||||
// Next page
|
||||
page++
|
||||
} while (parsed.second)
|
||||
|
||||
@ -544,7 +564,7 @@ class EHentai(override val id: Long,
|
||||
|
||||
fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp))
|
||||
|
||||
//Headers
|
||||
// Headers
|
||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
|
||||
|
||||
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||
@ -565,7 +585,7 @@ class EHentai(override val id: Long,
|
||||
chain.proceed(newReq)
|
||||
}.build()!!
|
||||
|
||||
//Filters
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
@ -673,11 +693,11 @@ class EHentai(override val id: Long,
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
return when (uri.pathSegments.firstOrNull()) {
|
||||
"g" -> {
|
||||
//Is already gallery page, do nothing
|
||||
// Is already gallery page, do nothing
|
||||
uri.toString()
|
||||
}
|
||||
"s" -> {
|
||||
//Is page, fetch gallery token and use that
|
||||
// Is page, fetch gallery token and use that
|
||||
getGalleryUrlFromPage(uri)
|
||||
}
|
||||
else -> null
|
||||
@ -713,7 +733,6 @@ class EHentai(override val id: Long,
|
||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
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 = "; ") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.LewdSource
|
||||
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.RaisedTag
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -32,8 +38,6 @@ import rx.Observable
|
||||
import rx.Single
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Man, I hate this source :(
|
||||
@ -61,8 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
private var tagIndexVersionCacheTime: Long = 0
|
||||
private fun tagIndexVersion(): Single<Long> {
|
||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
||||
return if (sCachedTagIndexVersion == null
|
||||
|| tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
return if (sCachedTagIndexVersion == null ||
|
||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedTagIndexVersion = it
|
||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
||||
@ -76,8 +80,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
private var galleryIndexVersionCacheTime: Long = 0
|
||||
private fun galleryIndexVersion(): Single<Long> {
|
||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
||||
return if (sCachedGalleryIndexVersion == null
|
||||
|| galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
return if (sCachedGalleryIndexVersion == null ||
|
||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedGalleryIndexVersion = it
|
||||
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
|
||||
* override this method.
|
||||
@ -423,5 +426,4 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
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 eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
@ -30,8 +39,8 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
override val metaClass = NHentaiSearchMetadata::class
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
//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 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
|
||||
return fetchLatestUpdates(page)
|
||||
}
|
||||
|
||||
@ -39,7 +48,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
|
||||
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> {
|
||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||
@ -246,7 +255,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
|
||||
override fun getFilterList() = FilterList(SortFilter(), filterLang())
|
||||
|
||||
//language filtering
|
||||
// language filtering
|
||||
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
||||
|
||||
class SortFilter : Filter.Sort(
|
||||
@ -305,7 +314,6 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
Pair("Chinese", " chinese")
|
||||
)
|
||||
|
||||
|
||||
val jsonParser by lazy {
|
||||
JsonParser()
|
||||
}
|
||||
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
@ -17,14 +22,15 @@ import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
// TODO Transform into delegated source
|
||||
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
|
||||
|
||||
//Support direct URL importing
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
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(
|
||||
ReleaseYearRangeFilter(),
|
||||
ReleaseYearYearFilter()
|
||||
|
@ -2,10 +2,14 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.net.Uri
|
||||
import com.kizitonwose.time.hours
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
@ -17,13 +21,18 @@ import exh.util.CachedField
|
||||
import exh.util.NakedTrie
|
||||
import exh.util.await
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.rx2.asSingle
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.*
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
@ -4,12 +4,16 @@ import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonParser
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
@ -23,7 +27,9 @@ import exh.search.Text
|
||||
import exh.util.await
|
||||
import exh.util.dropBlank
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import info.debatty.java.stringsimilarity.Levenshtein
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
@ -36,7 +42,6 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import kotlin.math.ceil
|
||||
|
||||
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
|
||||
/**
|
||||
@ -182,7 +187,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
"/result"
|
||||
} else {
|
||||
"/search"
|
||||
|
@ -30,7 +30,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
*/
|
||||
override val metaClass = HentaiCafeSearchMetadata::class
|
||||
|
||||
//Support direct URL importing
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
|
@ -29,7 +29,7 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
*/
|
||||
override val metaClass = PururinSearchMetadata::class
|
||||
|
||||
//Support direct URL importing
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||
|
@ -4,32 +4,28 @@ import android.net.Uri
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.util.system.asJsoup
|
||||
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.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
|
||||
override val metaClass = TsuminoSearchMetadata::class;
|
||||
override val metaClass = TsuminoSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
//Support direct URL importing
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
@ -5,21 +5,24 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
||||
import exh.isLewdSource
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.search.SearchEngine
|
||||
import exh.util.await
|
||||
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.filter
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
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.
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
|
||||
@ -26,7 +26,6 @@ abstract class LibraryHolder(
|
||||
*/
|
||||
abstract fun onSetValues(item: LibraryItem)
|
||||
|
||||
|
||||
/**
|
||||
* Called when an item is released.
|
||||
*
|
||||
@ -36,5 +35,4 @@ abstract class LibraryHolder(
|
||||
super.onItemReleased(position)
|
||||
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -235,4 +235,3 @@ class MangaInfoPresenter(
|
||||
return toInsert
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,17 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
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 exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
@ -34,6 +44,7 @@ import exh.ui.login.LoginController
|
||||
import exh.util.await
|
||||
import exh.util.trans
|
||||
import humanize.Humanize
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@ -41,7 +52,6 @@ import kotlinx.coroutines.withContext
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* EH Settings fragment
|
||||
@ -52,17 +62,17 @@ class SettingsEhController : SettingsController() {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private fun Preference<*>.reconfigure(): Boolean {
|
||||
//Listen for change commit
|
||||
// Listen for change commit
|
||||
asObservable()
|
||||
.skip(1) //Skip first as it is emitted immediately
|
||||
.take(1) //Only listen for first commit
|
||||
.skip(1) // Skip first as it is emitted immediately
|
||||
.take(1) // Only listen for first commit
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
//Only listen for first change commit
|
||||
// Only listen for first change commit
|
||||
WarnConfigureDialogController.uploadSettings(router)
|
||||
}
|
||||
|
||||
//Always return true to save changes
|
||||
// Always return true to save changes
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
|
||||
}.map { it.value.sourceId }.sorted()
|
||||
|
||||
// This method MUST be fast!
|
||||
fun isLewdSource(source: Long) = source in 6900..6999
|
||||
|| lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
fun isLewdSource(source: Long) = source in 6900..6999 ||
|
||||
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||
|
||||
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID
|
||||
|
@ -2,6 +2,8 @@ package exh
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
data class EXHSavedSearch(val name: String,
|
||||
val query: String,
|
||||
val filterList: FilterList)
|
||||
data class EXHSavedSearch(
|
||||
val name: String,
|
||||
val query: String,
|
||||
val filterList: FilterList
|
||||
)
|
||||
|
@ -16,20 +16,22 @@ class GalleryAdder {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
fun addGallery(url: String,
|
||||
fav: Boolean = false,
|
||||
forceSource: UrlImportableSource? = null,
|
||||
throttleFunc: () -> Unit = {}): GalleryAddEvent {
|
||||
fun addGallery(
|
||||
url: String,
|
||||
fav: Boolean = false,
|
||||
forceSource: UrlImportableSource? = null,
|
||||
throttleFunc: () -> Unit = {}
|
||||
): GalleryAddEvent {
|
||||
XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource)
|
||||
try {
|
||||
val uri = Uri.parse(url)
|
||||
|
||||
// Find matching source
|
||||
val source = if(forceSource != null) {
|
||||
val source = if (forceSource != null) {
|
||||
try {
|
||||
if (forceSource.matchesUri(uri)) forceSource
|
||||
else return GalleryAddEvent.Fail.UnknownType(url)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
return GalleryAddEvent.Fail.UnknownType(url)
|
||||
}
|
||||
@ -39,7 +41,7 @@ class GalleryAdder {
|
||||
.find {
|
||||
try {
|
||||
it.matchesUri(uri)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI match check error!", e)
|
||||
false
|
||||
}
|
||||
@ -49,7 +51,7 @@ class GalleryAdder {
|
||||
// Map URL to manga URL
|
||||
val realUrl = try {
|
||||
source.mapUrlToMangaUrl(uri)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI map-to-manga error!", e)
|
||||
null
|
||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||
@ -57,12 +59,12 @@ class GalleryAdder {
|
||||
// Clean URL
|
||||
val cleanedUrl = try {
|
||||
source.cleanMangaUrl(realUrl)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
XLog.e("Source URI clean error!", e)
|
||||
null
|
||||
} ?: 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()
|
||||
?: Manga.create(source.id).apply {
|
||||
this.url = cleanedUrl
|
||||
@ -71,7 +73,7 @@ class GalleryAdder {
|
||||
|
||||
// Insert created manga if not in DB before 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 {
|
||||
manga.id = it
|
||||
}
|
||||
@ -86,9 +88,9 @@ class GalleryAdder {
|
||||
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
|
||||
//Fetch and copy chapters
|
||||
// Fetch and copy chapters
|
||||
try {
|
||||
val chapterListObs = if(source is EHentai) {
|
||||
val chapterListObs = if (source is EHentai) {
|
||||
source.fetchChapterList(manga, throttleFunc)
|
||||
} else {
|
||||
source.fetchChapterList(manga)
|
||||
@ -102,10 +104,10 @@ class GalleryAdder {
|
||||
}
|
||||
|
||||
return GalleryAddEvent.Success(url, manga)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
XLog.w("Could not add gallery (url: $url)!", e)
|
||||
|
||||
if(e is EHentai.GalleryNotFoundException) {
|
||||
if (e is EHentai.GalleryNotFoundException) {
|
||||
return GalleryAddEvent.Fail.NotFound(url)
|
||||
}
|
||||
|
||||
@ -120,21 +122,25 @@ sealed class GalleryAddEvent {
|
||||
abstract val galleryUrl: String
|
||||
open val galleryTitle: String? = null
|
||||
|
||||
class Success(override val galleryUrl: String,
|
||||
val manga: Manga): GalleryAddEvent() {
|
||||
class Success(
|
||||
override val galleryUrl: String,
|
||||
val manga: Manga
|
||||
) : GalleryAddEvent() {
|
||||
override val galleryTitle = manga.title
|
||||
override val logMessage = "Added gallery: $galleryTitle"
|
||||
}
|
||||
|
||||
sealed class Fail: GalleryAddEvent() {
|
||||
class UnknownType(override val galleryUrl: String): Fail() {
|
||||
sealed class Fail : GalleryAddEvent() {
|
||||
class UnknownType(override val galleryUrl: String) : Fail() {
|
||||
override val logMessage = "Unknown gallery type for gallery: $galleryUrl"
|
||||
}
|
||||
|
||||
open class Error(override val galleryUrl: String,
|
||||
override val logMessage: String): Fail()
|
||||
open class Error(
|
||||
override val galleryUrl: String,
|
||||
override val logMessage: String
|
||||
) : Fail()
|
||||
|
||||
class NotFound(galleryUrl: String):
|
||||
class NotFound(galleryUrl: String) :
|
||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||
}
|
||||
}
|
@ -9,20 +9,19 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.system.jobScheduler
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.EXHMigrations
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.eh.EHentaiUpdateWorker
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.util.await
|
||||
import exh.util.cancellable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object DebugFunctions {
|
||||
val app: Application by injectLazy()
|
||||
@ -31,7 +30,7 @@ object DebugFunctions {
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
fun forceUpgradeMigration() {
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
prefs.eh_lastVersionCode().set(0)
|
||||
EXHMigrations.upgrade(prefs)
|
||||
}
|
||||
|
||||
@ -47,7 +46,7 @@ object DebugFunctions {
|
||||
|
||||
for (manga in allManga) {
|
||||
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
||||
if(meta != null) {
|
||||
if (meta != null) {
|
||||
// remove age flag
|
||||
meta.aged = false
|
||||
db.insertFlatMetadata(meta.flatten()).await()
|
||||
|
@ -8,8 +8,12 @@ import android.widget.HorizontalScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceScreen
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.ui.setting.*
|
||||
import eu.kanade.tachiyomi.util.preference.*
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||
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.full.declaredFunctions
|
||||
|
||||
@ -41,7 +45,7 @@ class SettingsDebugController : SettingsController() {
|
||||
view.text = "Function returned result:\n\n$result"
|
||||
MaterialDialog.Builder(context)
|
||||
.customView(hView, true)
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
|
||||
MaterialDialog.Builder(context)
|
||||
.customView(hView, true)
|
||||
@ -59,8 +63,8 @@ class SettingsDebugController : SettingsController() {
|
||||
title = it.name.replace('_', ' ').toLowerCase().capitalize()
|
||||
key = it.prefKey
|
||||
defaultValue = it.default
|
||||
summaryOn = if(it.default) "" else MODIFIED_TEXT
|
||||
summaryOff = if(it.default) MODIFIED_TEXT else ""
|
||||
summaryOn = if (it.default) "" else MODIFIED_TEXT
|
||||
summaryOff = if (it.default) MODIFIED_TEXT else ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package exh.eh
|
||||
|
||||
class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
|
||||
private val inc: Int = THROTTLE_INC) {
|
||||
class EHentaiThrottleManager(
|
||||
private val max: Int = THROTTLE_MAX,
|
||||
private val inc: Int = THROTTLE_INC
|
||||
) {
|
||||
private var lastThrottleTime: Long = 0
|
||||
var throttleTime: Long = 0
|
||||
private set
|
||||
|
||||
fun throttle() {
|
||||
//Throttle requests if necessary
|
||||
// Throttle requests if necessary
|
||||
val now = System.currentTimeMillis()
|
||||
val timeDiff = now - lastThrottleTime
|
||||
if(timeDiff < throttleTime)
|
||||
if (timeDiff < throttleTime)
|
||||
Thread.sleep(throttleTime - timeDiff)
|
||||
|
||||
if(throttleTime < max)
|
||||
if (throttleTime < max)
|
||||
throttleTime += inc
|
||||
|
||||
lastThrottleTime = System.currentTimeMillis()
|
||||
|
@ -8,10 +8,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import java.io.File
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
|
||||
data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
|
||||
|
||||
@ -61,7 +61,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
|
||||
val chainsAsChapters = chains.flatMap { it.chapters }
|
||||
|
||||
if(toDiscard.isNotEmpty()) {
|
||||
if (toDiscard.isNotEmpty()) {
|
||||
var new = false
|
||||
|
||||
// Copy chain chapters to curChapters
|
||||
@ -75,9 +75,9 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
|
||||
chain.chapters.map { chapter ->
|
||||
// Convert old style chapters to new style chapters if possible
|
||||
if(chapter.date_upload <= 0
|
||||
&& meta?.datePosted != null
|
||||
&& meta?.title != null) {
|
||||
if (chapter.date_upload <= 0 &&
|
||||
meta?.datePosted != null &&
|
||||
meta?.title != null) {
|
||||
chapter.name = meta!!.title!!
|
||||
chapter.date_upload = meta!!.datePosted!!
|
||||
}
|
||||
@ -92,7 +92,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
if (existing != null) {
|
||||
existing.read = existing.read || chapter.read
|
||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||
if(newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||
existing.last_page_read = newLastPageRead
|
||||
}
|
||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||
@ -107,7 +107,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
bookmark = chapter.bookmark
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -20,8 +20,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
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.system.jobScheduler
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.debug.DebugToggles
|
||||
@ -31,18 +31,23 @@ import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.util.await
|
||||
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.mapNotNull
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + Job()
|
||||
|
||||
@ -215,8 +220,8 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
val (acceptedRoot, discardedRoots, hasNew) =
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||
|
||||
if((new.isNotEmpty() && manga.id == acceptedRoot.manga.id)
|
||||
|| (hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
||||
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
||||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
||||
updatedManga += acceptedRoot.manga
|
||||
}
|
||||
|
||||
@ -235,7 +240,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
)
|
||||
)
|
||||
|
||||
if(updatedManga.isNotEmpty()) {
|
||||
if (updatedManga.isNotEmpty()) {
|
||||
updateNotifier.showResultNotification(updatedManga)
|
||||
}
|
||||
}
|
||||
@ -254,10 +259,10 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
|
||||
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
|
||||
return new to db.getChapters(manga).await()
|
||||
} catch(t: Throwable) {
|
||||
if(t is EHentai.GalleryNotFoundException) {
|
||||
} catch (t: Throwable) {
|
||||
if (t is EHentai.GalleryNotFoundException) {
|
||||
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
||||
if(meta != null) {
|
||||
if (meta != null) {
|
||||
// Age dead galleries
|
||||
logger.d("Aged %s - notfound", manga.id)
|
||||
meta.aged = true
|
||||
@ -286,18 +291,20 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
|
||||
private fun Context.baseBackgroundJobInfo(isTest: Boolean): 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())
|
||||
}
|
||||
|
||||
private fun Context.periodicBackgroundJobInfo(period: Long,
|
||||
requireCharging: Boolean,
|
||||
requireUnmetered: Boolean): JobInfo {
|
||||
private fun Context.periodicBackgroundJobInfo(
|
||||
period: Long,
|
||||
requireCharging: Boolean,
|
||||
requireUnmetered: Boolean
|
||||
): JobInfo {
|
||||
return baseBackgroundJobInfo(false)
|
||||
.setPeriodic(period)
|
||||
.setPersisted(true)
|
||||
.setRequiredNetworkType(
|
||||
if(requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||
else JobInfo.NETWORK_TYPE_ANY)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@ -321,7 +328,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
|
||||
fun launchBackgroundTest(context: Context) {
|
||||
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!")
|
||||
} else {
|
||||
logger.d("Successfully scheduled background test job!")
|
||||
@ -344,7 +351,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
||||
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!")
|
||||
} else {
|
||||
logger.d("Successfully scheduled background update job!")
|
||||
|
@ -1,7 +1,7 @@
|
||||
package exh.eh
|
||||
|
||||
data class EHentaiUpdaterStats(
|
||||
val startTime: Long,
|
||||
val possibleUpdates: Int,
|
||||
val updateCount: Int
|
||||
val startTime: Long,
|
||||
val possibleUpdates: Int,
|
||||
val updateCount: Int
|
||||
)
|
@ -1,3 +1,3 @@
|
||||
package exh.eh
|
||||
|
||||
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable): RuntimeException(cause)
|
||||
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable) : RuntimeException(cause)
|
||||
|
@ -3,9 +3,6 @@ package exh.eh
|
||||
import android.util.SparseArray
|
||||
import androidx.core.util.AtomicFile
|
||||
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.File
|
||||
import java.io.FileNotFoundException
|
||||
@ -13,6 +10,18 @@ import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.concurrent.thread
|
||||
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
|
||||
@ -23,9 +32,9 @@ import kotlin.coroutines.CoroutineContext
|
||||
* @author nulldev
|
||||
*/
|
||||
class MemAutoFlushingLookupTable<T>(
|
||||
file: File,
|
||||
private val serializer: EntrySerializer<T>,
|
||||
private val debounceTimeMs: Long = 3000
|
||||
file: File,
|
||||
private val serializer: EntrySerializer<T>,
|
||||
private val debounceTimeMs: Long = 3000
|
||||
) : CoroutineScope, Closeable {
|
||||
/**
|
||||
* The context of this scope.
|
||||
@ -49,7 +58,7 @@ class MemAutoFlushingLookupTable<T>(
|
||||
private val atomicFile = AtomicFile(file)
|
||||
|
||||
private val shutdownHook = thread(start = false) {
|
||||
if(!flushed) writeSynchronously()
|
||||
if (!flushed) writeSynchronously()
|
||||
}
|
||||
|
||||
init {
|
||||
@ -62,9 +71,9 @@ class MemAutoFlushingLookupTable<T>(
|
||||
var readIter = 0
|
||||
while (true) {
|
||||
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
|
||||
if(readIter == byteCount) return true
|
||||
if (readIter == byteCount) return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,16 +83,16 @@ class MemAutoFlushingLookupTable<T>(
|
||||
atomicFile.openRead().buffered().use { input ->
|
||||
val bb = ByteBuffer.allocate(8)
|
||||
|
||||
while(true) {
|
||||
if(!input.requireBytes(bb.array(), 8)) break
|
||||
while (true) {
|
||||
if (!input.requireBytes(bb.array(), 8)) break
|
||||
val k = bb.getInt(0)
|
||||
val size = bb.getInt(4)
|
||||
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)))
|
||||
}
|
||||
}
|
||||
} catch(e: FileNotFoundException) {
|
||||
} catch (e: FileNotFoundException) {
|
||||
XLog.d("Lookup table not found!", e)
|
||||
// Ignored
|
||||
}
|
||||
@ -97,11 +106,11 @@ class MemAutoFlushingLookupTable<T>(
|
||||
flushed = false
|
||||
launch {
|
||||
delay(debounceTimeMs)
|
||||
if(id != writeCounter) return@launch
|
||||
if (id != writeCounter) return@launch
|
||||
|
||||
mutex.withLock {
|
||||
// Second check inside of mutex to prevent dupe writes
|
||||
if(id != writeCounter) return@launch
|
||||
if (id != writeCounter) return@launch
|
||||
withContext(NonCancellable) {
|
||||
writeSynchronously()
|
||||
|
||||
@ -118,7 +127,7 @@ class MemAutoFlushingLookupTable<T>(
|
||||
val fos = atomicFile.startWrite()
|
||||
try {
|
||||
val out = fos.buffered()
|
||||
for(i in 0 until table.size()) {
|
||||
for (i in 0 until table.size()) {
|
||||
val k = table.keyAt(i)
|
||||
val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8)
|
||||
bb.putInt(0, k)
|
||||
@ -128,7 +137,7 @@ class MemAutoFlushingLookupTable<T>(
|
||||
}
|
||||
out.flush()
|
||||
atomicFile.finishWrite(fos)
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
atomicFile.failWrite(fos)
|
||||
throw t
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
@RealmClass
|
||||
open class FavoriteEntry : RealmObject() {
|
||||
|
@ -17,18 +17,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
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.EHentaiUpdateWorker
|
||||
import exh.util.ignore
|
||||
import exh.util.trans
|
||||
import kotlin.concurrent.thread
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import rx.subjects.BehaviorSubject
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class FavoritesSyncHelper(val context: Context) {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
@ -55,7 +58,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
@Synchronized
|
||||
fun runSync() {
|
||||
if(status.value !is FavoritesSyncStatus.Idle) {
|
||||
if (status.value !is FavoritesSyncStatus.Idle) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -65,8 +68,8 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
private fun beginSync() {
|
||||
//Check if logged in
|
||||
if(!prefs.enableExhentai().getOrDefault()) {
|
||||
// Check if logged in
|
||||
if (!prefs.enableExhentai().getOrDefault()) {
|
||||
status.onNext(FavoritesSyncStatus.Error("Please log in!"))
|
||||
return
|
||||
}
|
||||
@ -76,9 +79,9 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
val seenManga = HashSet<Long>(libraryManga.size)
|
||||
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()
|
||||
status.onNext(FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(it, inCategories))
|
||||
@ -89,20 +92,20 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
//Download remote favorites
|
||||
// Download remote favorites
|
||||
val favorites = try {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server"))
|
||||
exh.fetchFavorites()
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
}
|
||||
|
||||
val errorList = mutableListOf<String>()
|
||||
|
||||
try {
|
||||
//Take wake + wifi locks
|
||||
// Take wake + wifi locks
|
||||
ignore { wakeLock?.release() }
|
||||
wakeLock = ignore {
|
||||
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
@ -124,20 +127,20 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
db.inTransaction {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes"))
|
||||
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
|
||||
val localChanges = if(prefs.eh_readOnlySync().getOrDefault()) {
|
||||
null //Do not build local changes if they are not going to be applied
|
||||
val localChanges = if (prefs.eh_readOnlySync().getOrDefault()) {
|
||||
null // Do not build local changes if they are not going to be applied
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Calculating local changes"))
|
||||
storage.getChangedDbEntries(realm)
|
||||
}
|
||||
|
||||
//Apply remote categories
|
||||
// Apply remote categories
|
||||
status.onNext(FavoritesSyncStatus.Processing("Updating category names"))
|
||||
applyRemoteCategories(errorList, favorites.second)
|
||||
|
||||
//Apply change sets
|
||||
// Apply change sets
|
||||
applyChangeSetToLocal(errorList, remoteChanges)
|
||||
if(localChanges != null)
|
||||
if (localChanges != null)
|
||||
applyChangeSetToRemote(errorList, localChanges)
|
||||
|
||||
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
|
||||
@ -150,16 +153,16 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
launchUI {
|
||||
theContext.toast("Sync complete!")
|
||||
}
|
||||
} catch(e: IgnoredException) {
|
||||
//Do not display error as this error has already been reported
|
||||
logger.w( "Ignoring exception!", e)
|
||||
} catch (e: IgnoredException) {
|
||||
// Do not display error as this error has already been reported
|
||||
logger.w("Ignoring exception!", e)
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}"))
|
||||
logger.e( "Sync error!", e)
|
||||
logger.e("Sync error!", e)
|
||||
return
|
||||
} finally {
|
||||
//Release wake + wifi locks
|
||||
// Release wake + wifi locks
|
||||
ignore {
|
||||
wakeLock?.release()
|
||||
wakeLock = null
|
||||
@ -175,7 +178,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if(errorList.isEmpty())
|
||||
if (errorList.isEmpty())
|
||||
status.onNext(FavoritesSyncStatus.Idle())
|
||||
else
|
||||
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
|
||||
@ -195,31 +198,31 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
Category.create(remote).apply {
|
||||
order = index
|
||||
|
||||
//Going through categories list from front to back
|
||||
//If category does not exist, list size <= category index
|
||||
//Thus, we can just add it here and not worry about indexing
|
||||
// Going through categories list from front to back
|
||||
// If category does not exist, list size <= category index
|
||||
// Thus, we can just add it here and not worry about indexing
|
||||
newLocalCategories += this
|
||||
}
|
||||
}
|
||||
|
||||
if(local.name != remote) {
|
||||
if (local.name != remote) {
|
||||
changed = true
|
||||
|
||||
local.name = remote
|
||||
}
|
||||
}
|
||||
|
||||
//Ensure consistent ordering
|
||||
// Ensure consistent ordering
|
||||
newLocalCategories.forEachIndexed { index, category ->
|
||||
if(category.order != index) {
|
||||
if (category.order != index) {
|
||||
changed = true
|
||||
|
||||
category.order = index
|
||||
}
|
||||
}
|
||||
|
||||
//Only insert categories if changed
|
||||
if(changed)
|
||||
// Only insert categories if changed
|
||||
if (changed)
|
||||
db.insertCategories(newLocalCategories).executeAsBlocking()
|
||||
}
|
||||
|
||||
@ -236,10 +239,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
.build())
|
||||
.build()
|
||||
|
||||
if(!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
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
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||
@ -251,7 +254,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean {
|
||||
var success = false
|
||||
|
||||
for(i in 1 .. retryCount) {
|
||||
for (i in 1..retryCount) {
|
||||
try {
|
||||
val resp = exh.client.newCall(request).execute()
|
||||
|
||||
@ -260,7 +263,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
break
|
||||
}
|
||||
} 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) {
|
||||
//Apply removals
|
||||
if(changeSet.removed.isNotEmpty()) {
|
||||
// Apply removals
|
||||
if (changeSet.removed.isNotEmpty()) {
|
||||
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
|
||||
|
||||
val formBody = FormBody.Builder()
|
||||
.add("ddact", "delete")
|
||||
.add("apply", "Apply")
|
||||
|
||||
//Add change set to form
|
||||
// Add change set to form
|
||||
changeSet.removed.forEach {
|
||||
formBody.add("modifygids[]", it.gid)
|
||||
}
|
||||
@ -286,10 +289,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
.post(formBody.build())
|
||||
.build()
|
||||
|
||||
if(!explicitlyRetryExhRequest(10, request)) {
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to delete galleries from the remote servers!"
|
||||
|
||||
if(prefs.eh_lenientSync().getOrDefault()) {
|
||||
if (prefs.eh_lenientSync().getOrDefault()) {
|
||||
errorList += errorString
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||
@ -298,7 +301,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
//Apply additions
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
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) {
|
||||
val removedManga = mutableListOf<Manga>()
|
||||
|
||||
//Apply removals
|
||||
// Apply removals
|
||||
changeSet.removed.forEachIndexed { index, it ->
|
||||
status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library"))
|
||||
val url = it.getUrl()
|
||||
|
||||
//Consider both EX and EH sources
|
||||
// Consider both EX and EH sources
|
||||
listOf(db.getManga(url, EXH_SOURCE_ID),
|
||||
db.getManga(url, EH_SOURCE_ID)).forEach {
|
||||
val manga = it.executeAsBlocking()
|
||||
|
||||
if(manga?.favorite == true) {
|
||||
if (manga?.favorite == true) {
|
||||
manga.favorite = false
|
||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||
removedManga += manga
|
||||
@ -339,7 +342,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>()
|
||||
val categories = db.getCategories().executeAsBlocking()
|
||||
|
||||
//Apply additions
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
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()
|
||||
|
||||
//Import using gallery adder
|
||||
// Import using gallery adder
|
||||
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
|
||||
true,
|
||||
exh,
|
||||
throttleManager::throttle)
|
||||
|
||||
if(result is GalleryAddEvent.Fail) {
|
||||
if(result is GalleryAddEvent.Fail.NotFound) {
|
||||
if (result is GalleryAddEvent.Fail) {
|
||||
if (result is GalleryAddEvent.Fail.NotFound) {
|
||||
XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl())
|
||||
// Skip this gallery, it no longer exists
|
||||
return@forEachIndexed
|
||||
@ -365,13 +368,13 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
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
|
||||
} else {
|
||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||
throw IgnoredException()
|
||||
}
|
||||
} else if(result is GalleryAddEvent.Success) {
|
||||
} else if (result is GalleryAddEvent.Success) {
|
||||
insertedMangaCategories += MangaCategory.create(result.manga,
|
||||
categories[it.category]) to result.manga
|
||||
}
|
||||
@ -385,8 +388,8 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun needWarnThrottle()
|
||||
= throttleManager.throttleTime >= THROTTLE_WARN
|
||||
fun needWarnThrottle() =
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
|
||||
class IgnoredException : RuntimeException()
|
||||
|
||||
@ -399,12 +402,14 @@ sealed class FavoritesSyncStatus(val message: String) {
|
||||
class Error(message: String) : FavoritesSyncStatus(message)
|
||||
class Idle : FavoritesSyncStatus("Waiting for sync to start")
|
||||
sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) {
|
||||
class MangaInMultipleCategories(val manga: Manga,
|
||||
val categories: List<Category>):
|
||||
class MangaInMultipleCategories(
|
||||
val manga: Manga,
|
||||
val categories: List<Category>
|
||||
) :
|
||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||
}
|
||||
class Initializing : FavoritesSyncStatus("Initializing sync")
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if(isThrottle)
|
||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if (isThrottle)
|
||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||
else
|
||||
message)
|
||||
|
@ -20,8 +20,8 @@ class LocalFavoritesStorage {
|
||||
|
||||
fun getRealm() = Realm.getInstance(realmConfig)
|
||||
|
||||
fun getChangedDbEntries(realm: Realm)
|
||||
= getChangedEntries(realm,
|
||||
fun getChangedDbEntries(realm: Realm) =
|
||||
getChangedEntries(realm,
|
||||
parseToFavoriteEntries(
|
||||
loadDbCategories(
|
||||
db.getFavoriteMangas()
|
||||
@ -31,8 +31,8 @@ class LocalFavoritesStorage {
|
||||
)
|
||||
)
|
||||
|
||||
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>)
|
||||
= getChangedEntries(realm,
|
||||
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
|
||||
getChangedEntries(realm,
|
||||
parseToFavoriteEntries(
|
||||
entries.asSequence().map {
|
||||
Pair(it.fav, it.manga.apply {
|
||||
@ -51,10 +51,10 @@ class LocalFavoritesStorage {
|
||||
)
|
||||
)
|
||||
|
||||
//Delete old snapshot
|
||||
// Delete old snapshot
|
||||
realm.delete(FavoriteEntry::class.java)
|
||||
|
||||
//Insert new snapshots
|
||||
// Insert new snapshots
|
||||
realm.copyToRealm(dbMangas.toList())
|
||||
}
|
||||
|
||||
@ -80,18 +80,18 @@ class LocalFavoritesStorage {
|
||||
return ChangeSet(added, removed)
|
||||
}
|
||||
|
||||
private fun Realm.queryRealmForEntry(entry: FavoriteEntry)
|
||||
= where(FavoriteEntry::class.java)
|
||||
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
|
||||
where(FavoriteEntry::class.java)
|
||||
.equalTo(FavoriteEntry::gid.name, entry.gid)
|
||||
.equalTo(FavoriteEntry::token.name, entry.token)
|
||||
.equalTo(FavoriteEntry::category.name, entry.category)
|
||||
.findFirst()
|
||||
|
||||
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry)
|
||||
= list.find {
|
||||
it.gid == entry.gid
|
||||
&& it.token == entry.token
|
||||
&& it.category == entry.category
|
||||
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
|
||||
list.find {
|
||||
it.gid == entry.gid &&
|
||||
it.token == entry.token &&
|
||||
it.category == entry.category
|
||||
}
|
||||
|
||||
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
|
||||
@ -105,8 +105,8 @@ class LocalFavoritesStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>)
|
||||
= manga.filter {
|
||||
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
|
||||
manga.filter {
|
||||
validateDbManga(it.second)
|
||||
}.mapNotNull {
|
||||
FavoriteEntry().apply {
|
||||
@ -115,18 +115,20 @@ class LocalFavoritesStorage {
|
||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||
category = it.first
|
||||
|
||||
if(this.category > MAX_CATEGORIES)
|
||||
if (this.category > MAX_CATEGORIES)
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDbManga(manga: Manga)
|
||||
= manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
private fun validateDbManga(manga: Manga) =
|
||||
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||
|
||||
companion object {
|
||||
const val MAX_CATEGORIES = 9
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeSet(val added: List<FavoriteEntry>,
|
||||
val removed: List<FavoriteEntry>)
|
||||
data class ChangeSet(
|
||||
val added: List<FavoriteEntry>,
|
||||
val removed: List<FavoriteEntry>
|
||||
)
|
||||
|
@ -4,42 +4,46 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
||||
import java.security.MessageDigest
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.vepta.vdm.ByteCursor
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import java.security.MessageDigest
|
||||
|
||||
private typealias HashedTerm = ByteArray
|
||||
|
||||
private data class DataPair(val offset: Long, val length: Int)
|
||||
private data class Node(val keys: List<ByteArray>,
|
||||
val datas: List<DataPair>,
|
||||
val subnodeAddresses: List<Long>)
|
||||
private data class Node(
|
||||
val keys: List<ByteArray>,
|
||||
val datas: List<DataPair>,
|
||||
val subnodeAddresses: List<Long>
|
||||
)
|
||||
|
||||
/**
|
||||
* Kotlin port of the hitomi.la search algorithm
|
||||
* @author NerdNumber9
|
||||
*/
|
||||
class HitomiNozomi(private val client: OkHttpClient,
|
||||
private val tagIndexVersion: Long,
|
||||
private val galleriesIndexVersion: Long) {
|
||||
class HitomiNozomi(
|
||||
private val client: OkHttpClient,
|
||||
private val tagIndexVersion: Long,
|
||||
private val galleriesIndexVersion: Long
|
||||
) {
|
||||
fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
|
||||
val replacedQuery = query.replace('_', ' ')
|
||||
|
||||
if(':' in replacedQuery) {
|
||||
if (':' in replacedQuery) {
|
||||
val sides = replacedQuery.split(':')
|
||||
val namespace = sides[0]
|
||||
var tag = sides[1]
|
||||
|
||||
var area: String? = namespace
|
||||
var language = "all"
|
||||
if(namespace == "female" || namespace == "male") {
|
||||
if (namespace == "female" || namespace == "male") {
|
||||
area = "tag"
|
||||
tag = replacedQuery
|
||||
} else if(namespace == "language") {
|
||||
} else if (namespace == "language") {
|
||||
area = null
|
||||
language = tag
|
||||
tag = "index"
|
||||
@ -52,7 +56,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
val field = "galleries"
|
||||
|
||||
return getNodeAtAddress(field, 0).flatMap { node ->
|
||||
if(node == null) {
|
||||
if (node == null) {
|
||||
Single.just(null)
|
||||
} else {
|
||||
BSearch(field, key, node).flatMap { data ->
|
||||
@ -67,12 +71,12 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
}
|
||||
|
||||
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
||||
if(data == null)
|
||||
if (data == null)
|
||||
return Single.just(emptyList())
|
||||
|
||||
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
||||
val (offset, length) = data
|
||||
if(length > 100000000 || length <= 0)
|
||||
if (length > 100000000 || length <= 0)
|
||||
return Single.just(emptyList())
|
||||
|
||||
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
||||
@ -82,7 +86,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { inbuf ->
|
||||
if(inbuf.isEmpty())
|
||||
if (inbuf.isEmpty())
|
||||
return@map emptyList<Int>()
|
||||
|
||||
val view = ByteCursor(inbuf)
|
||||
@ -90,13 +94,13 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
|
||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||
|
||||
if(numberOfGalleryIds > 10000000
|
||||
|| numberOfGalleryIds <= 0
|
||||
|| inbuf.size != expectedLength) {
|
||||
if (numberOfGalleryIds > 10000000 ||
|
||||
numberOfGalleryIds <= 0 ||
|
||||
inbuf.size != expectedLength) {
|
||||
return@map emptyList<Int>()
|
||||
}
|
||||
|
||||
(1 .. numberOfGalleryIds).map {
|
||||
(1..numberOfGalleryIds).map {
|
||||
view.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
@ -105,12 +109,12 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
||||
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
|
||||
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 dv2i = dv2[i].toInt() and 0xFF
|
||||
if(dv1i < dv2i)
|
||||
if (dv1i < dv2i)
|
||||
return -1
|
||||
else if(dv1i > dv2i)
|
||||
else if (dv1i > dv2i)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
@ -119,9 +123,9 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
|
||||
var cmpResult = -1
|
||||
var lastI = 0
|
||||
for(nodeKey in node.keys) {
|
||||
for (nodeKey in node.keys) {
|
||||
cmpResult = compareByteArrays(key, nodeKey)
|
||||
if(cmpResult <= 0) break
|
||||
if (cmpResult <= 0) break
|
||||
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)
|
||||
}
|
||||
|
||||
val (there, where) = locateKey(key, node)
|
||||
if(there) {
|
||||
if (there) {
|
||||
return Single.just(node.datas[where])
|
||||
} else if(isLeaf(node)) {
|
||||
} else if (isLeaf(node)) {
|
||||
return Single.just(null)
|
||||
}
|
||||
|
||||
@ -154,20 +158,20 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
|
||||
val numberOfKeys = view.nextInt()
|
||||
|
||||
val keys = (1 .. numberOfKeys).map {
|
||||
val keys = (1..numberOfKeys).map {
|
||||
val keySize = view.nextInt()
|
||||
view.next(keySize)
|
||||
}
|
||||
|
||||
val numberOfDatas = view.nextInt()
|
||||
val datas = (1 .. numberOfDatas).map {
|
||||
val datas = (1..numberOfDatas).map {
|
||||
val offset = view.nextLong()
|
||||
val length = view.nextInt()
|
||||
DataPair(offset, length)
|
||||
}
|
||||
|
||||
val numberOfSubnodeAddresses = B + 1
|
||||
val subnodeAddresses = (1 .. numberOfSubnodeAddresses).map {
|
||||
val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
|
||||
view.nextLong()
|
||||
}
|
||||
|
||||
@ -176,7 +180,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
|
||||
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
|
||||
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"
|
||||
}
|
||||
|
||||
@ -187,7 +191,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
}
|
||||
.onErrorReturn { ByteArray(0) }
|
||||
.map { nodedata ->
|
||||
if(nodedata.isNotEmpty()) {
|
||||
if (nodedata.isNotEmpty()) {
|
||||
decodeNode(nodedata)
|
||||
} else null
|
||||
}.toSingle()
|
||||
@ -195,7 +199,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
|
||||
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
|
||||
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"
|
||||
}
|
||||
|
||||
@ -206,7 +210,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
.map { resp ->
|
||||
val body = resp.body!!.bytes()
|
||||
val cursor = ByteCursor(body)
|
||||
(1 .. body.size / 4).map {
|
||||
(1..body.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
}.toSingle()
|
||||
@ -234,7 +238,6 @@ class HitomiNozomi(private val client: OkHttpClient,
|
||||
.build())
|
||||
}
|
||||
|
||||
|
||||
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
||||
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
||||
.asObservableSuccess()
|
||||
|
@ -9,16 +9,16 @@ class CrashlyticsPrinter(private val logLevel: Int) : Printer {
|
||||
* Print log in new line.
|
||||
*
|
||||
* @param logLevel the level of log
|
||||
* @param tag the tag of log
|
||||
* @param msg the msg of log
|
||||
* @param tag the tag of log
|
||||
* @param msg the msg of log
|
||||
*/
|
||||
override fun println(logLevel: Int, tag: String?, msg: String?) {
|
||||
if(logLevel >= this.logLevel) {
|
||||
if (logLevel >= this.logLevel) {
|
||||
try {
|
||||
Crashlytics.log(logLevel, tag, msg)
|
||||
} catch (t: Throwable) {
|
||||
// Crash in debug if shit like this happens
|
||||
if(BuildConfig.DEBUG) throw t
|
||||
if (BuildConfig.DEBUG) throw t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ import android.content.Context
|
||||
import android.text.Html
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.ms_square.debugoverlay.DataObserver
|
||||
import com.ms_square.debugoverlay.OverlayModule
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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()}
|
||||
""".trimIndent()
|
||||
|
||||
private fun Boolean.asEnabledString() = if(this) "enabled" else "disabled"
|
||||
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
|
||||
}
|
@ -2,7 +2,7 @@ package exh.log
|
||||
|
||||
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)) {
|
||||
val xLogger = XLog.tag("EHNetwork")
|
||||
.nst()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package exh.metadata
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Metadata utils
|
||||
@ -35,13 +35,12 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
|
||||
null
|
||||
else
|
||||
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) }
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,14 @@ import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
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.plusAssign
|
||||
import java.util.Date
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
var gId: String?
|
||||
@ -27,7 +29,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
var datePosted: Long? = null
|
||||
var parent: String? = null
|
||||
var visible: String? = null //Not a boolean
|
||||
var visible: String? = null // Not a boolean
|
||||
var language: String? = null
|
||||
var translated: Boolean? = null
|
||||
var size: Long? = null
|
||||
@ -47,23 +49,23 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
//No title bug?
|
||||
val titleObj = if(Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
||||
// No title bug?
|
||||
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
||||
altTitle ?: title
|
||||
else
|
||||
title
|
||||
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 {
|
||||
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()
|
||||
|
||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
//We default to completed
|
||||
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
// We default to completed
|
||||
manga.status = SManga.COMPLETED
|
||||
title?.let { t ->
|
||||
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()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
||||
@ -85,7 +87,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
visible?.let { detailsDesc += "Visible: $it\n" }
|
||||
language?.let {
|
||||
detailsDesc += "Language: $it"
|
||||
if(translated == true) detailsDesc += " TR"
|
||||
if (translated == true) detailsDesc += " TR"
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
|
||||
@ -114,10 +116,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
const val EH_GENRE_NAMESPACE = "genre"
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
|
||||
private fun splitGalleryUrl(url: String)
|
||||
= url.let {
|
||||
//Only parse URL if is full URL
|
||||
val pathSegments = if(it.startsWith("http"))
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http"))
|
||||
Uri.parse(it).pathSegments
|
||||
else
|
||||
it.split('/')
|
||||
@ -129,10 +131,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
fun galleryToken(url: String) =
|
||||
splitGalleryUrl(url)[2]
|
||||
|
||||
fun normalizeUrl(url: String)
|
||||
= idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
fun normalizeUrl(url: String) =
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
|
||||
fun idAndTokenToUrl(id: String, token: String)
|
||||
= "/g/$id/$token/?nw=always"
|
||||
fun idAndTokenToUrl(id: String, token: String) =
|
||||
"/g/$id/$token/?nw=always"
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -21,7 +21,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.artist = artist
|
||||
manga.author = artist
|
||||
|
||||
//Not available
|
||||
// Not available
|
||||
manga.status = SManga.UNKNOWN
|
||||
|
||||
val detailsDesc = "Title: $title\n" +
|
||||
@ -49,7 +49,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
const val BASE_URL = "https://hentai.cafe"
|
||||
|
||||
fun hcIdFromUrl(url: String)
|
||||
= url.split("/").last { it.isNotBlank() }
|
||||
fun hcIdFromUrl(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.EX_DATE_FORMAT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.plusAssign
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class HitomiSearchMetadata: RaisedSearchMetadata() {
|
||||
class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
var url get() = hlId?.let { urlFromHlId(it) }
|
||||
set(a) {
|
||||
a?.let {
|
||||
@ -62,10 +62,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
||||
detailsDesc += "Language: ${it.capitalize()}\n"
|
||||
}
|
||||
|
||||
if(series.isNotEmpty())
|
||||
if (series.isNotEmpty())
|
||||
detailsDesc += "Series: ${series.joinToString()}\n"
|
||||
|
||||
if(characters.isNotEmpty())
|
||||
if (characters.isNotEmpty())
|
||||
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
||||
|
||||
uploadDate?.let {
|
||||
@ -74,7 +74,7 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
||||
|
||||
manga.status = SManga.UNKNOWN
|
||||
|
||||
//Copy tags -> genres
|
||||
// Copy tags -> genres
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
val tagsDesc = tagsToDescription()
|
||||
@ -92,10 +92,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
||||
const val LTN_BASE_URL = "https://ltn.hitomi.la"
|
||||
const val BASE_URL = "https://hitomi.la"
|
||||
|
||||
fun hlIdFromUrl(url: String)
|
||||
= url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
fun hlIdFromUrl(url: String) =
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
|
||||
fun urlFromHlId(id: String)
|
||||
= "$BASE_URL/galleries/$id.html"
|
||||
fun urlFromHlId(id: String) =
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,14 @@ package exh.metadata.metadata
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
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.nullIfBlank
|
||||
import exh.plusAssign
|
||||
import java.util.Date
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
|
||||
@ -39,10 +41,10 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
override fun copyTo(manga: SManga) {
|
||||
nhId?.let { manga.url = nhIdToPath(it) }
|
||||
|
||||
if(mediaId != null) {
|
||||
if (mediaId != null) {
|
||||
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
|
||||
typeToExtension(if(hqThumbs) coverImageType else thumbnailImageType)?.let {
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if(hqThumbs)
|
||||
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
|
||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs)
|
||||
"cover"
|
||||
else "thumb"}.$it"
|
||||
}
|
||||
@ -50,21 +52,21 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
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 {
|
||||
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||
}
|
||||
|
||||
var category: String? = null
|
||||
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()
|
||||
|
||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
//We default to completed
|
||||
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
// We default to completed
|
||||
manga.status = SManga.COMPLETED
|
||||
englishTitle?.let { t ->
|
||||
ONGOING_SUFFIX.find {
|
||||
@ -106,14 +108,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when(t) {
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun nhUrlToId(url: String)
|
||||
= url.split("/").last { it.isNotBlank() }.toLong()
|
||||
fun nhUrlToId(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
|
||||
fun nhIdToPath(id: Long) = "/g/$id/"
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
manga.title = it
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
if(altTitles.isNotEmpty())
|
||||
if (altTitles.isNotEmpty())
|
||||
titleDesc += "Alternate Titles: \n" + altTitles
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
@ -58,7 +58,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
|
||||
status?.let {
|
||||
manga.status = when(it) {
|
||||
manga.status = when (it) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed", "Suspended" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
@ -70,7 +70,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
detailsDesc += "Rating: %.2\n".format(it)
|
||||
}
|
||||
|
||||
//Copy tags -> genres
|
||||
// Copy tags -> genres
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
val tagsDesc = tagsToDescription()
|
||||
@ -80,15 +80,14 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
private const val TITLE_TYPE_ALT = 1
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
private fun splitGalleryUrl(url: String)
|
||||
= url.let {
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
@ -97,13 +96,13 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
|
||||
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),
|
||||
it(PERV_EDEN_IT_SOURCE_ID);
|
||||
|
||||
companion object {
|
||||
fun source(id: Long)
|
||||
= values().find { it.id == id }
|
||||
fun source(id: Long) =
|
||||
values().find { it.id == id }
|
||||
?: throw IllegalArgumentException("Unknown source ID: $id!")
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n"}
|
||||
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
|
||||
pages?.let { detailsDesc += "Length: $it pages\n" }
|
||||
fileSize?.let { detailsDesc += "Size: $it\n" }
|
||||
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
|
||||
|
@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.EX_DATE_FORMAT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.plusAssign
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
var tmId: Int? = null
|
||||
@ -51,15 +51,15 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
collection?.let { detailsDesc += "Collection: $it\n" }
|
||||
group?.let { detailsDesc += "Group: $it\n" }
|
||||
val parodiesString = parody.joinToString()
|
||||
if(parodiesString.isNotEmpty()) {
|
||||
if (parodiesString.isNotEmpty()) {
|
||||
detailsDesc += "Parody: $parodiesString\n"
|
||||
}
|
||||
val charactersString = character.joinToString()
|
||||
if(charactersString.isNotEmpty()) {
|
||||
if (charactersString.isNotEmpty()) {
|
||||
detailsDesc += "Character: $charactersString\n"
|
||||
}
|
||||
|
||||
//Copy tags -> genres
|
||||
// Copy tags -> genres
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
val tagsDesc = tagsToDescription()
|
||||
@ -76,8 +76,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
val BASE_URL = "https://www.tsumino.com"
|
||||
|
||||
fun tmIdFromUrl(url: String)
|
||||
= Uri.parse(url).lastPathSegment
|
||||
fun tmIdFromUrl(url: String) =
|
||||
Uri.parse(url).lastPathSegment
|
||||
|
||||
fun mangaUrlFromId(id: String) = "/Book/Info/$id"
|
||||
|
||||
|
@ -5,19 +5,19 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import exh.metadata.sql.models.SearchMetadata
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import kotlin.reflect.KClass
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class FlatMetadata(
|
||||
val metadata: SearchMetadata,
|
||||
val tags: List<SearchTag>,
|
||||
val titles: List<SearchTitle>
|
||||
val metadata: SearchMetadata,
|
||||
val tags: List<SearchTag>,
|
||||
val titles: List<SearchTitle>
|
||||
) {
|
||||
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
|
||||
|
||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>)
|
||||
= RaisedSearchMetadata.raiseFlattenGson
|
||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
|
||||
RaisedSearchMetadata.raiseFlattenGson
|
||||
.fromJson(metadata.extra, clazz.java).apply {
|
||||
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
|
||||
val single = Single.fromCallable {
|
||||
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
|
||||
if(meta != null) {
|
||||
if (meta != null) {
|
||||
val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
|
||||
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
|
||||
|
||||
|
@ -30,18 +30,18 @@ abstract class RaisedSearchMetadata {
|
||||
|
||||
fun replaceTitleOfType(type: Int, newTitle: String?) {
|
||||
titles.removeAll { it.type == type }
|
||||
if(newTitle != null) titles += RaisedTitle(newTitle, type)
|
||||
if (newTitle != null) titles += RaisedTitle(newTitle, type)
|
||||
}
|
||||
|
||||
abstract fun copyTo(manga: SManga)
|
||||
|
||||
fun tagsToGenreString()
|
||||
= tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.joinToString { (if(it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
fun tagsToGenreString() =
|
||||
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
|
||||
fun tagsToDescription()
|
||||
= StringBuilder("Tags:\n").apply {
|
||||
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
fun tagsToDescription() =
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
@ -49,7 +49,7 @@ abstract class RaisedSearchMetadata {
|
||||
groupedTags.forEach { namespace, tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if(namespace != null) {
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
@ -125,8 +125,8 @@ abstract class RaisedSearchMetadata {
|
||||
* @param property the metadata for the property.
|
||||
* @return the property value.
|
||||
*/
|
||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>)
|
||||
= thisRef.getTitleOfType(type)
|
||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
|
||||
thisRef.getTitleOfType(type)
|
||||
|
||||
/**
|
||||
* 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 value the value to set.
|
||||
*/
|
||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?)
|
||||
= thisRef.replaceTitleOfType(type, value)
|
||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
data class RaisedTag(val namespace: String?,
|
||||
val name: String,
|
||||
val type: Int)
|
||||
data class RaisedTag(
|
||||
val namespace: String?,
|
||||
val name: String,
|
||||
val type: Int
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
data class RaisedTitle(
|
||||
val title: String,
|
||||
val type: Int = 0
|
||||
val title: String,
|
||||
val type: Int = 0
|
||||
)
|
@ -2,19 +2,19 @@ package exh.metadata.sql.models
|
||||
|
||||
data class SearchMetadata(
|
||||
// Manga ID this gallery is linked to
|
||||
val mangaId: Long,
|
||||
val mangaId: Long,
|
||||
|
||||
// Gallery uploader
|
||||
val uploader: String?,
|
||||
val uploader: String?,
|
||||
|
||||
// Extra data attached to this metadata, in JSON format
|
||||
val extra: String,
|
||||
val extra: String,
|
||||
|
||||
// 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
|
||||
val extraVersion: Int
|
||||
val extraVersion: Int
|
||||
) {
|
||||
// Transient information attached to this piece of metadata, useful for caching
|
||||
var transientCache: Map<String, Any>? = null
|
||||
|
@ -2,17 +2,17 @@ package exh.metadata.sql.models
|
||||
|
||||
data class SearchTag(
|
||||
// Tag identifier, unique
|
||||
val id: Long?,
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this tag is attached to
|
||||
val mangaId: Long,
|
||||
val mangaId: Long,
|
||||
|
||||
// Tag namespace
|
||||
val namespace: String?,
|
||||
val namespace: String?,
|
||||
|
||||
// Tag name
|
||||
val name: String,
|
||||
val name: String,
|
||||
|
||||
// Tag type
|
||||
val type: Int
|
||||
val type: Int
|
||||
)
|
@ -2,14 +2,14 @@ package exh.metadata.sql.models
|
||||
|
||||
data class SearchTitle(
|
||||
// Title identifier, unique
|
||||
val id: Long?,
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this title is attached to
|
||||
val mangaId: Long,
|
||||
val mangaId: Long,
|
||||
|
||||
// Title
|
||||
val title: String,
|
||||
val title: String,
|
||||
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
val type: Int
|
||||
val type: Int
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ package exh.metadata.sql.queries
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
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.tables.SearchMetadataTable
|
||||
|
||||
|
@ -4,8 +4,6 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
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.tables.SearchTitleTable
|
||||
|
||||
|
@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||
if(!response.isSuccessful) {
|
||||
if (!response.isSuccessful) {
|
||||
response.interceptAsHtml { doc ->
|
||||
// Find captcha
|
||||
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
||||
|
@ -1,4 +1,6 @@
|
||||
package exh.search
|
||||
|
||||
class Namespace(var namespace: String,
|
||||
var tag: Text? = null) : QueryComponent()
|
||||
class Namespace(
|
||||
var namespace: String,
|
||||
var tag: Text? = null
|
||||
) : QueryComponent()
|
||||
|
@ -7,8 +7,10 @@ import exh.metadata.sql.tables.SearchTitleTable
|
||||
class SearchEngine {
|
||||
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
||||
|
||||
fun textToSubQueries(namespace: String?,
|
||||
component: Text?): Pair<String, List<String>>? {
|
||||
fun textToSubQueries(
|
||||
namespace: String?,
|
||||
component: Text?
|
||||
): Pair<String, List<String>>? {
|
||||
val maybeLenientComponent = component?.let {
|
||||
if (!it.exact)
|
||||
it.asLenientTagQueries()
|
||||
@ -22,20 +24,20 @@ class SearchEngine {
|
||||
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
||||
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
||||
}
|
||||
return if(namespace != null) {
|
||||
return if (namespace != null) {
|
||||
var query = """
|
||||
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
||||
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
||||
""".trimIndent()
|
||||
val params = mutableListOf(escapeLike(namespace))
|
||||
if(componentTagQuery != null) {
|
||||
if (componentTagQuery != null) {
|
||||
query += "\n AND ${componentTagQuery.first}"
|
||||
params += componentTagQuery.second
|
||||
}
|
||||
|
||||
"$query)" to params
|
||||
} else if(component != null) {
|
||||
} else if (component != null) {
|
||||
// Match title + tags
|
||||
val tagQuery = """
|
||||
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 exclude = mutableListOf<Pair<String, List<String>>>()
|
||||
|
||||
for(component in q) {
|
||||
val query = if(component is Text) {
|
||||
for (component in q) {
|
||||
val query = if (component is Text) {
|
||||
textToSubQueries(null, component)
|
||||
} else if(component is Namespace) {
|
||||
if(component.namespace == "uploader") {
|
||||
} else if (component is Namespace) {
|
||||
if (component.namespace == "uploader") {
|
||||
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
|
||||
whereParams += component.tag!!.rawTextEscapedForLike()
|
||||
null
|
||||
} else {
|
||||
if(component.tag!!.components.size > 0) {
|
||||
//Match namespace + tags
|
||||
if (component.tag!!.components.size > 0) {
|
||||
// Match namespace + tags
|
||||
textToSubQueries(component.namespace, component.tag)
|
||||
} else {
|
||||
//Perform namespace search
|
||||
// Perform namespace search
|
||||
textToSubQueries(component.namespace, null)
|
||||
}
|
||||
}
|
||||
} else error("Unknown query component!")
|
||||
|
||||
if(query != null) {
|
||||
(if(component.excluded) exclude else include) += query
|
||||
if (query != null) {
|
||||
(if (component.excluded) exclude else include) += query
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,14 +99,13 @@ class SearchEngine {
|
||||
completeParams += pair.second
|
||||
}
|
||||
|
||||
|
||||
exclude.forEach {
|
||||
wheres += """
|
||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||
""".trimIndent()
|
||||
whereParams += it.second
|
||||
}
|
||||
if(wheres.isNotEmpty()) {
|
||||
if (wheres.isNotEmpty()) {
|
||||
completeParams += whereParams
|
||||
baseQuery += "\nWHERE\n"
|
||||
baseQuery += wheres.joinToString("\nAND\n")
|
||||
@ -126,7 +127,7 @@ class SearchEngine {
|
||||
var nextIsExact = false
|
||||
|
||||
fun flushText() {
|
||||
if(queuedRawText.isNotEmpty()) {
|
||||
if (queuedRawText.isNotEmpty()) {
|
||||
queuedText += StringTextComponent(queuedRawText.toString())
|
||||
queuedRawText.setLength(0)
|
||||
}
|
||||
@ -150,24 +151,24 @@ class SearchEngine {
|
||||
}
|
||||
}
|
||||
|
||||
for(char in query.toLowerCase()) {
|
||||
if(char == '"') {
|
||||
for (char in query.toLowerCase()) {
|
||||
if (char == '"') {
|
||||
inQuotes = !inQuotes
|
||||
} else if(enableWildcard && (char == '?' || char == '_')) {
|
||||
} else if (enableWildcard && (char == '?' || char == '_')) {
|
||||
flushText()
|
||||
queuedText.add(SingleWildcard(char.toString()))
|
||||
} else if(enableWildcard && (char == '*' || char == '%')) {
|
||||
} else if (enableWildcard && (char == '*' || char == '%')) {
|
||||
flushText()
|
||||
queuedText.add(MultiWildcard(char.toString()))
|
||||
} else if(char == '-') {
|
||||
} else if (char == '-') {
|
||||
nextIsExcluded = true
|
||||
} else if(char == '$') {
|
||||
} else if (char == '$') {
|
||||
nextIsExact = true
|
||||
} else if(char == ':') {
|
||||
} else if (char == ':') {
|
||||
flushText()
|
||||
var flushed = flushToText().rawTextOnly()
|
||||
//Map tag aliases
|
||||
flushed = when(flushed) {
|
||||
// Map tag aliases
|
||||
flushed = when (flushed) {
|
||||
"a" -> "artist"
|
||||
"c", "char" -> "character"
|
||||
"f" -> "female"
|
||||
@ -179,7 +180,7 @@ class SearchEngine {
|
||||
else -> flushed
|
||||
}
|
||||
namespace = Namespace(flushed, null)
|
||||
} else if(char == ' ' && !inQuotes) {
|
||||
} else if (char == ' ' && !inQuotes) {
|
||||
flushAll()
|
||||
} else {
|
||||
queuedRawText.append(char)
|
||||
@ -197,7 +198,6 @@ class SearchEngine {
|
||||
return string.replace("\\", "\\\\")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package exh.search
|
||||
import exh.plusAssign
|
||||
import exh.search.SearchEngine.Companion.escapeLike
|
||||
|
||||
class Text: QueryComponent() {
|
||||
class Text : QueryComponent() {
|
||||
val components = mutableListOf<TextComponent>()
|
||||
|
||||
private var query: String? = null
|
||||
@ -12,26 +12,26 @@ class Text: QueryComponent() {
|
||||
private var rawText: String? = null
|
||||
|
||||
fun asQuery(): String {
|
||||
if(query == null) {
|
||||
if (query == null) {
|
||||
query = rBaseBuilder().toString()
|
||||
}
|
||||
return query!!
|
||||
}
|
||||
|
||||
fun asLenientTitleQuery(): String {
|
||||
if(lenientTitleQuery == null) {
|
||||
if (lenientTitleQuery == null) {
|
||||
lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString()
|
||||
}
|
||||
return lenientTitleQuery!!
|
||||
}
|
||||
|
||||
fun asLenientTagQueries(): List<String> {
|
||||
if(lenientTagQueries == null) {
|
||||
if (lenientTagQueries == null) {
|
||||
lenientTagQueries = listOf(
|
||||
//Match beginning of tag
|
||||
// Match beginning of tag
|
||||
rBaseBuilder().append("%").toString(),
|
||||
//Tag word matcher (that matches multiple words)
|
||||
//Can't make it match a single word in Realm :(
|
||||
// Tag word matcher (that matches multiple words)
|
||||
// Can't make it match a single word in Realm :(
|
||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||
rBaseBuilder().append(" ").toString()
|
||||
@ -42,8 +42,8 @@ class Text: QueryComponent() {
|
||||
|
||||
fun rBaseBuilder(): StringBuilder {
|
||||
val builder = StringBuilder()
|
||||
for(component in components) {
|
||||
when(component) {
|
||||
for (component in components) {
|
||||
when (component) {
|
||||
is StringTextComponent -> builder += escapeLike(component.value)
|
||||
is SingleWildcard -> builder += "_"
|
||||
is MultiWildcard -> builder += "%"
|
||||
@ -52,7 +52,7 @@ class Text: QueryComponent() {
|
||||
return builder
|
||||
}
|
||||
|
||||
fun rawTextOnly() = if(rawText != null)
|
||||
fun rawTextOnly() = if (rawText != null)
|
||||
rawText!!
|
||||
else {
|
||||
rawText = components
|
||||
|
@ -8,13 +8,19 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.ui.smartsearch.SmartSearchPresenter
|
||||
import exh.util.await
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
val extraSearchParams: String? = null): CoroutineScope {
|
||||
class SmartSearchEngine(
|
||||
parentContext: CoroutineContext,
|
||||
val extraSearchParams: String? = null
|
||||
) : CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
@ -29,7 +35,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
val eligibleManga = supervisorScope {
|
||||
queries.map { query ->
|
||||
async(Dispatchers.Default) {
|
||||
val builtQuery = if(extraSearchParams != null) {
|
||||
val builtQuery = if (extraSearchParams != null) {
|
||||
"$query ${extraSearchParams.trim()}"
|
||||
} else query
|
||||
|
||||
@ -51,7 +57,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
|
||||
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
|
||||
val eligibleManga = supervisorScope {
|
||||
val searchQuery = if(extraSearchParams != null) {
|
||||
val searchQuery = if (extraSearchParams != null) {
|
||||
"$title ${extraSearchParams.trim()}"
|
||||
} else title
|
||||
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
||||
@ -71,7 +77,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||
|
||||
if(splitCleanedTitle.isEmpty()) {
|
||||
if (splitCleanedTitle.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@ -99,7 +105,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
|
||||
// Remove text in brackets
|
||||
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)
|
||||
}
|
||||
|
||||
@ -127,7 +133,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
}.toMap()
|
||||
|
||||
// Reverse pairs if reading backwards
|
||||
if(!readForward) {
|
||||
if (!readForward) {
|
||||
val tmp = openingBracketPairs
|
||||
openingBracketPairs = closingBracketPairs
|
||||
closingBracketPairs = tmp
|
||||
@ -136,16 +142,16 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
||||
val depthPairs = bracketPairs.map { 0 }.toMutableList()
|
||||
|
||||
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]
|
||||
if(openingBracketDepthIndex != null) {
|
||||
if (openingBracketDepthIndex != null) {
|
||||
depthPairs[openingBracketDepthIndex]++
|
||||
} else {
|
||||
val closingBracketDepthIndex = closingBracketPairs[c]
|
||||
if(closingBracketDepthIndex != null) {
|
||||
if (closingBracketDepthIndex != null) {
|
||||
depthPairs[closingBracketDepthIndex]--
|
||||
} else {
|
||||
if(depthPairs.all { it <= 0 }) {
|
||||
if (depthPairs.all { it <= 0 }) {
|
||||
result.append(c)
|
||||
} else {
|
||||
// In brackets, do not append to result
|
||||
|
@ -1,29 +1,32 @@
|
||||
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 okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
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.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* 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 filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
private fun ensureDelegateCompatible() {
|
||||
if(versionId != delegate.versionId
|
||||
|| lang != delegate.lang) {
|
||||
if (versionId != delegate.versionId ||
|
||||
lang != delegate.lang) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,19 @@ package exh.source
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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 okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class EnhancedHttpSource(val originalSource: HttpSource,
|
||||
val enchancedSource: HttpSource): HttpSource() {
|
||||
class EnhancedHttpSource(
|
||||
val originalSource: HttpSource,
|
||||
val enchancedSource: HttpSource
|
||||
) : HttpSource() {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
@ -16,16 +22,16 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* 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 filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response)
|
||||
= throw UnsupportedOperationException("Should never be called!")
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* 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 filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList)
|
||||
= source().fetchSearchManga(page, query, filters)
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
|
||||
/**
|
||||
* 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 manga the manga of the chapter.
|
||||
*/
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga)
|
||||
= source().prepareNewChapter(chapter, manga)
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
@ -211,7 +217,7 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
||||
override fun getFilterList() = source().getFilterList()
|
||||
|
||||
private fun source(): HttpSource {
|
||||
return if(prefs.eh_delegateSources().getOrDefault()) {
|
||||
return if (prefs.eh_delegateSources().getOrDefault()) {
|
||||
enchancedSource
|
||||
} else {
|
||||
originalSource
|
||||
|
@ -7,14 +7,14 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import timber.log.Timber
|
||||
import kotlin.concurrent.thread
|
||||
import timber.log.Timber
|
||||
|
||||
class ConfiguringDialogController : DialogController() {
|
||||
private var materialDialog: MaterialDialog? = null
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
if(savedViewState == null)
|
||||
if (savedViewState == null)
|
||||
thread {
|
||||
try {
|
||||
EHConfigurator().configureAll()
|
||||
@ -62,4 +62,3 @@ class ConfiguringDialogController : DialogController() {
|
||||
router.popController(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,11 +24,13 @@ class EHConfigurator {
|
||||
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
|
||||
.addHeader("Cookie", cookiesHeader(sp))
|
||||
|
||||
private fun EHentai.execProfileActions(action: String,
|
||||
name: String,
|
||||
set: String,
|
||||
sp: Int)
|
||||
= configuratorClient.newCall(requestWithCreds(sp)
|
||||
private fun EHentai.execProfileActions(
|
||||
action: String,
|
||||
name: String,
|
||||
set: String,
|
||||
sp: Int
|
||||
) =
|
||||
configuratorClient.newCall(requestWithCreds(sp)
|
||||
.url(uconfigUrl)
|
||||
.post(FormBody.Builder()
|
||||
.add("profile_action", action)
|
||||
@ -44,7 +46,7 @@ class EHConfigurator {
|
||||
val ehSource = sources.get(EH_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()
|
||||
.url(HATH_PERKS_URL)
|
||||
.build())
|
||||
@ -56,13 +58,13 @@ class EHConfigurator {
|
||||
val name = it.child(0).text().toLowerCase()
|
||||
val purchased = it.child(2).getElementsByTag("form").isEmpty()
|
||||
|
||||
when(name) {
|
||||
//Thumbnail rows
|
||||
when (name) {
|
||||
// Thumbnail rows
|
||||
"more thumbs" -> hathPerks.moreThumbs = purchased
|
||||
"thumbs up" -> hathPerks.thumbsUp = purchased
|
||||
"all thumbs" -> hathPerks.allThumbs = purchased
|
||||
|
||||
//Pagination sizing
|
||||
// Pagination sizing
|
||||
"paging enlargement i" -> hathPerks.pagingEnlargementI = purchased
|
||||
"paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased
|
||||
"paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased
|
||||
@ -76,45 +78,45 @@ class EHConfigurator {
|
||||
}
|
||||
|
||||
fun configure(source: EHentai, hathPerks: EHHathPerksResponse) {
|
||||
//Delete old app profiles
|
||||
// Delete old app profiles
|
||||
val scanReq = source.requestWithCreds().url(source.uconfigUrl).build()
|
||||
val resp = configuratorClient.newCall(scanReq).execute().asJsoup()
|
||||
var lastDoc = resp
|
||||
resp.select(PROFILE_SELECTOR).forEach {
|
||||
if(it.text() == PROFILE_NAME) {
|
||||
if (it.text() == PROFILE_NAME) {
|
||||
val id = it.attr("value")
|
||||
//Delete old profile
|
||||
// Delete old profile
|
||||
lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup()
|
||||
}
|
||||
}
|
||||
|
||||
//Find available profile slot
|
||||
val availableProfiles = (1 .. 3).toMutableList()
|
||||
// Find available profile slot
|
||||
val availableProfiles = (1..3).toMutableList()
|
||||
lastDoc.select(PROFILE_SELECTOR).forEach {
|
||||
availableProfiles.remove(it.attr("value").toInt())
|
||||
}
|
||||
|
||||
//No profile slots left :(
|
||||
if(availableProfiles.isEmpty())
|
||||
// No profile slots left :(
|
||||
if (availableProfiles.isEmpty())
|
||||
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 response = source.execProfileActions("create",
|
||||
PROFILE_NAME,
|
||||
slot.toString(),
|
||||
1)
|
||||
|
||||
//Build new profile
|
||||
// Build new profile
|
||||
val form = EhUConfigBuilder().build(hathPerks)
|
||||
|
||||
//Send new profile to server
|
||||
// Send new profile to server
|
||||
configuratorClient.newCall(source.requestWithCreds(sp = slot)
|
||||
.url(source.uconfigUrl)
|
||||
.post(form)
|
||||
.build()).execute()
|
||||
|
||||
//Persist slot + sk
|
||||
// Persist slot + sk
|
||||
source.spPref().set(slot)
|
||||
|
||||
val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find {
|
||||
@ -127,18 +129,18 @@ class EHConfigurator {
|
||||
it.startsWith("hath_perks=")
|
||||
}?.removePrefix("hath_perks=")?.substringBefore(';')
|
||||
|
||||
if(keyCookie != null)
|
||||
if (keyCookie != null)
|
||||
prefs.eh_settingsKey().set(keyCookie)
|
||||
if(sessionCookie != null)
|
||||
if (sessionCookie != null)
|
||||
prefs.eh_sessionCookie().set(sessionCookie)
|
||||
if(hathPerksCookie != null)
|
||||
if (hathPerksCookie != null)
|
||||
prefs.eh_hathPerksCookies().set(hathPerksCookie)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE_NAME = "TachiyomiEH App"
|
||||
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 PROFILE_SELECTOR = "[name=profile_set] > option"
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ class EHHathPerksResponse {
|
||||
var pagingEnlargementII = false
|
||||
var pagingEnlargementIII = false
|
||||
|
||||
override fun toString()
|
||||
= "EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
|
||||
override fun toString() =
|
||||
"EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ class EhUConfigBuilder {
|
||||
fun build(hathPerks: EHHathPerksResponse): FormBody {
|
||||
val configItems = mutableListOf<ConfigItem>()
|
||||
|
||||
configItems += when(prefs.imageQuality()
|
||||
configItems += when (prefs.imageQuality()
|
||||
.getOrDefault()
|
||||
.toLowerCase()) {
|
||||
"ovrs_2400" -> Entry.ImageSize.`2400`
|
||||
@ -23,17 +23,17 @@ class EhUConfigBuilder {
|
||||
else -> Entry.ImageSize.AUTO
|
||||
}
|
||||
|
||||
configItems += if(prefs.useHentaiAtHome().getOrDefault())
|
||||
configItems += if (prefs.useHentaiAtHome().getOrDefault())
|
||||
Entry.UseHentaiAtHome.YES
|
||||
else
|
||||
Entry.UseHentaiAtHome.NO
|
||||
|
||||
configItems += if(prefs.useJapaneseTitle().getOrDefault())
|
||||
configItems += if (prefs.useJapaneseTitle().getOrDefault())
|
||||
Entry.TitleDisplayLanguage.JAPANESE
|
||||
else
|
||||
Entry.TitleDisplayLanguage.DEFAULT
|
||||
|
||||
configItems += if(prefs.eh_useOriginalImages().getOrDefault())
|
||||
configItems += if (prefs.eh_useOriginalImages().getOrDefault())
|
||||
Entry.UseOriginalImages.YES
|
||||
else
|
||||
Entry.UseOriginalImages.NO
|
||||
@ -56,7 +56,7 @@ class EhUConfigBuilder {
|
||||
configItems += Entry.UseMPV()
|
||||
configItems += Entry.ShowPopularRightNowPane()
|
||||
|
||||
//Actually build form body
|
||||
// Actually build form body
|
||||
val formBody = FormBody.Builder()
|
||||
configItems.forEach {
|
||||
formBody.add(it.key, it.value)
|
||||
@ -67,14 +67,14 @@ class EhUConfigBuilder {
|
||||
}
|
||||
|
||||
object Entry {
|
||||
enum class UseHentaiAtHome(override val value: String): ConfigItem {
|
||||
enum class UseHentaiAtHome(override val value: String) : ConfigItem {
|
||||
YES("0"),
|
||||
NO("1");
|
||||
|
||||
override val key = "uh"
|
||||
}
|
||||
|
||||
enum class ImageSize(override val value: String): ConfigItem {
|
||||
enum class ImageSize(override val value: String) : ConfigItem {
|
||||
AUTO("0"),
|
||||
`2400`("5"),
|
||||
`1600`("4"),
|
||||
@ -85,20 +85,20 @@ object Entry {
|
||||
override val key = "xr"
|
||||
}
|
||||
|
||||
enum class TitleDisplayLanguage(override val value: String): ConfigItem {
|
||||
enum class TitleDisplayLanguage(override val value: String) : ConfigItem {
|
||||
DEFAULT("0"),
|
||||
JAPANESE("1");
|
||||
|
||||
override val key = "tl"
|
||||
}
|
||||
|
||||
//Locked to extended mode as that's what the parser and toplists use
|
||||
class DisplayMode: ConfigItem {
|
||||
// Locked to extended mode as that's what the parser and toplists use
|
||||
class DisplayMode : ConfigItem {
|
||||
override val key = "dm"
|
||||
override val value = "2"
|
||||
}
|
||||
|
||||
enum class SearchResultsCount(override val value: String): ConfigItem {
|
||||
enum class SearchResultsCount(override val value: String) : ConfigItem {
|
||||
`25`("0"),
|
||||
`50`("1"),
|
||||
`100`("2"),
|
||||
@ -107,7 +107,7 @@ object Entry {
|
||||
override val key = "rc"
|
||||
}
|
||||
|
||||
enum class ThumbnailRows(override val value: String): ConfigItem {
|
||||
enum class ThumbnailRows(override val value: String) : ConfigItem {
|
||||
`4`("0"),
|
||||
`10`("1"),
|
||||
`20`("2"),
|
||||
@ -116,21 +116,21 @@ object Entry {
|
||||
override val key = "tr"
|
||||
}
|
||||
|
||||
enum class UseOriginalImages(override val value: String): ConfigItem {
|
||||
enum class UseOriginalImages(override val value: String) : ConfigItem {
|
||||
NO("0"),
|
||||
YES("1");
|
||||
|
||||
override val key = "oi"
|
||||
}
|
||||
|
||||
//Locked to no MPV as that's what the parser uses
|
||||
class UseMPV: ConfigItem {
|
||||
// Locked to no MPV as that's what the parser uses
|
||||
class UseMPV : ConfigItem {
|
||||
override val key = "qb"
|
||||
override val value = "0"
|
||||
}
|
||||
|
||||
//Locked to no popular pane as we can't parse it
|
||||
class ShowPopularRightNowPane: ConfigItem {
|
||||
// Locked to no popular pane as we can't parse it
|
||||
class ShowPopularRightNowPane : ConfigItem {
|
||||
override val key = "pp"
|
||||
override val value = "1"
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ class WarnConfigureDialogController : DialogController() {
|
||||
|
||||
companion object {
|
||||
fun uploadSettings(router: Router) {
|
||||
if(Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
|
||||
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
|
||||
WarnConfigureDialogController().showDialog(router)
|
||||
else
|
||||
ConfiguringDialogController().showDialog(router)
|
||||
|
@ -1,18 +1,18 @@
|
||||
package exh.ui
|
||||
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
typealias LoadingHandle = String
|
||||
|
||||
/**
|
||||
* Class used to manage loader UIs
|
||||
*/
|
||||
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope {
|
||||
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.Main + parentContext
|
||||
|
||||
private val openLoadingHandles = mutableListOf<LoadingHandle>()
|
||||
@ -25,7 +25,7 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
|
||||
handle to (openLoadingHandles.size == 1)
|
||||
}
|
||||
|
||||
if(shouldUpdateLoadingStatus) {
|
||||
if (shouldUpdateLoadingStatus) {
|
||||
launch {
|
||||
updateLoadingStatus(true)
|
||||
}
|
||||
@ -36,13 +36,13 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
|
||||
|
||||
@Synchronized
|
||||
fun closeProgressBar(handle: LoadingHandle?) {
|
||||
if(handle == null) return
|
||||
if (handle == null) return
|
||||
|
||||
val shouldUpdateLoadingStatus = synchronized(this) {
|
||||
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
|
||||
}
|
||||
|
||||
if(shouldUpdateLoadingStatus) {
|
||||
if (shouldUpdateLoadingStatus) {
|
||||
launch {
|
||||
updateLoadingStatus(false)
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
|
||||
abstract val layoutId: Int
|
||||
|
@ -1,6 +1,5 @@
|
||||
package exh.ui.batchadd
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -44,14 +43,14 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy {
|
||||
progressSubscriptions.clear()
|
||||
if(it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||
showProgress(this)
|
||||
progressSubscriptions += presenter.progressRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.combineLatest(presenter.progressTotalRelay, { progress, total ->
|
||||
//Show hide dismiss button
|
||||
// Show hide dismiss button
|
||||
progress_dismiss_btn.visibility =
|
||||
if(progress == total)
|
||||
if (progress == total)
|
||||
View.VISIBLE
|
||||
else View.GONE
|
||||
|
||||
@ -79,7 +78,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
|
||||
}?.let {
|
||||
progressSubscriptions += it
|
||||
}
|
||||
} else if(it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||
hideProgress(this)
|
||||
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 addGalleries(galleries: String) {
|
||||
//Check text box has content
|
||||
if(galleries.isBlank()) {
|
||||
// Check text box has content
|
||||
if (galleries.isBlank()) {
|
||||
noGalleriesSpecified()
|
||||
return
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import exh.GalleryAdder
|
||||
import exh.metadata.nullIfBlank
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
||||
class BatchAddPresenter : BasePresenter<BatchAddController>() {
|
||||
|
||||
private val galleryAdder by lazy { GalleryAdder() }
|
||||
|
||||
@ -34,7 +34,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
||||
|
||||
splitGalleries.forEachIndexed { i, s ->
|
||||
val result = galleryAdder.addGallery(s, true)
|
||||
if(result is GalleryAddEvent.Success) {
|
||||
if (result is GalleryAddEvent.Success) {
|
||||
succeeded.add(s)
|
||||
} else {
|
||||
failed.add(s)
|
||||
@ -46,7 +46,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
||||
}) + " " + result.logMessage)
|
||||
}
|
||||
|
||||
//Show report
|
||||
// Show report
|
||||
val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)"
|
||||
eventRelay?.call(summary)
|
||||
}
|
||||
|
@ -7,21 +7,23 @@ import android.webkit.WebView
|
||||
import androidx.annotation.RequiresApi
|
||||
import eu.kanade.tachiyomi.util.system.asJsoup
|
||||
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
|
||||
import java.nio.charset.Charset
|
||||
import org.jsoup.nodes.DataNode
|
||||
import org.jsoup.nodes.Element
|
||||
import java.nio.charset.Charset
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class AutoSolvingWebViewClient(activity: BrowserActionActivity,
|
||||
verifyComplete: (String) -> Boolean,
|
||||
injectScript: String?,
|
||||
headers: Map<String, String>)
|
||||
: HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
||||
class AutoSolvingWebViewClient(
|
||||
activity: BrowserActionActivity,
|
||||
verifyComplete: (String) -> Boolean,
|
||||
injectScript: String?,
|
||||
headers: Map<String, String>
|
||||
) :
|
||||
HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
||||
|
||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||
// Inject our custom script into the recaptcha iframes
|
||||
val lastPathSegment = request.url.pathSegments.lastOrNull()
|
||||
if(lastPathSegment == "anchor" || lastPathSegment == "bframe") {
|
||||
if (lastPathSegment == "anchor" || lastPathSegment == "bframe") {
|
||||
val oReq = request.toOkHttpRequest()
|
||||
val response = activity.httpClient.newCall(oReq).execute()
|
||||
val doc = response.asJsoup()
|
||||
|
@ -4,16 +4,18 @@ import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
|
||||
open class BasicWebViewClient(protected val activity: BrowserActionActivity,
|
||||
protected val verifyComplete: (String) -> Boolean,
|
||||
private val injectScript: String?) : WebViewClient() {
|
||||
open class BasicWebViewClient(
|
||||
protected val activity: BrowserActionActivity,
|
||||
protected val verifyComplete: (String) -> Boolean,
|
||||
private val injectScript: String?
|
||||
) : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
if(verifyComplete(url)) {
|
||||
if (verifyComplete(url)) {
|
||||
activity.finish()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,12 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
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.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
@ -22,6 +27,9 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import exh.source.DelegatedHttpSource
|
||||
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 okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
@ -33,10 +41,6 @@ import rx.Single
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.Serializable
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class BrowserActionActivity : AppCompatActivity() {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
@ -58,8 +62,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
|
||||
|
||||
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
||||
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
|
||||
val source = if(originalSource != null) {
|
||||
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
|
||||
val source = if (originalSource != null) {
|
||||
originalSource as? ActionCompletionVerifier
|
||||
?: run {
|
||||
(originalSource as? HttpSource)?.let {
|
||||
@ -72,24 +76,24 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
it.value.joinToString(",")
|
||||
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||
|
||||
val cookies: HashMap<String, String>?
|
||||
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
val cookies: HashMap<String, String>? =
|
||||
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
||||
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
||||
|
||||
val verifyComplete = if(source != null) {
|
||||
val verifyComplete = if (source != null) {
|
||||
source::verifyComplete!!
|
||||
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
||||
|
||||
if(verifyComplete == null || url == null) {
|
||||
if (verifyComplete == null || url == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val actionStr = actionName ?: "Solve captcha"
|
||||
|
||||
toolbar.title = if(source != null) {
|
||||
toolbar.title = if (source != null) {
|
||||
"${source.name}: $actionStr"
|
||||
} else actionStr
|
||||
|
||||
@ -115,13 +119,13 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
webview.webChromeClient = object : WebChromeClient() {
|
||||
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
|
||||
if(message.startsWith("exh-")) {
|
||||
if (message.startsWith("exh-")) {
|
||||
loadedInners++
|
||||
// Wait for both inner scripts to be loaded
|
||||
if(loadedInners >= 2) {
|
||||
if (loadedInners >= 2) {
|
||||
// Attempt to autosolve captcha
|
||||
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault() &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
webview.post {
|
||||
// 10 seconds to auto-solve captcha
|
||||
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
|
||||
@ -141,7 +145,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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
|
||||
credentialsObservable = httpClient.newCall(Request.Builder()
|
||||
// Rob demo credentials
|
||||
@ -196,11 +200,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@JavascriptInterface
|
||||
fun callback(result: String?, loopId: String, stage: Int) {
|
||||
if(loopId != currentLoopId) return
|
||||
if (loopId != currentLoopId) return
|
||||
|
||||
when(stage) {
|
||||
when (stage) {
|
||||
STAGE_CHECKBOX -> {
|
||||
if(result!!.toBoolean()) {
|
||||
if (result!!.toBoolean()) {
|
||||
webview.postDelayed({
|
||||
getAudioButtonLocation(loopId)
|
||||
}, 250)
|
||||
@ -211,7 +215,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
||||
if(result != null) {
|
||||
if (result != null) {
|
||||
val splitResult = result.split(" ").map { it.toFloat() }
|
||||
val origX = splitResult[0]
|
||||
val origY = splitResult[1]
|
||||
@ -231,11 +235,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
STAGE_DOWNLOAD_AUDIO -> {
|
||||
if(result != null) {
|
||||
if (result != null) {
|
||||
Timber.d("Got audio URL: $result")
|
||||
performRecognize(result)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe ({
|
||||
.subscribe({
|
||||
Timber.d("Got audio transcript: $it")
|
||||
webview.post {
|
||||
typeResult(loopId, it!!
|
||||
@ -253,7 +257,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
STAGE_TYPE_RESULT -> {
|
||||
if(result!!.toBoolean()) {
|
||||
if (result!!.toBoolean()) {
|
||||
// Fail if captcha still not solved after 1.5s
|
||||
strictValidationStartTime = System.currentTimeMillis() + 1500
|
||||
} else {
|
||||
@ -293,7 +297,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun doStageCheckbox(loopId: String) {
|
||||
if(loopId != currentLoopId) return
|
||||
if (loopId != currentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
(function() {
|
||||
@ -415,27 +419,26 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
doStageCheckbox(loopId)
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@JavascriptInterface
|
||||
fun validateCaptchaCallback(result: Boolean, loopId: String) {
|
||||
if(loopId != validateCurrentLoopId) return
|
||||
if (loopId != validateCurrentLoopId) return
|
||||
|
||||
if(result) {
|
||||
if (result) {
|
||||
Timber.d("Captcha solved!")
|
||||
webview.post {
|
||||
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
||||
}
|
||||
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
|
||||
if(asbtn != null) {
|
||||
if (asbtn != null) {
|
||||
webview.post {
|
||||
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val savedStrictValidationStartTime = strictValidationStartTime
|
||||
if(savedStrictValidationStartTime != null
|
||||
&& System.currentTimeMillis() > savedStrictValidationStartTime) {
|
||||
if (savedStrictValidationStartTime != null &&
|
||||
System.currentTimeMillis() > savedStrictValidationStartTime) {
|
||||
captchaSolveFail()
|
||||
} else {
|
||||
webview.postDelayed({
|
||||
@ -447,7 +450,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun runValidateCaptcha(loopId: String) {
|
||||
if(loopId != validateCurrentLoopId) return
|
||||
if (loopId != validateCurrentLoopId) return
|
||||
|
||||
webview.evaluateJavascript("""
|
||||
(function() {
|
||||
@ -624,12 +627,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
fun launchCaptcha(context: Context,
|
||||
source: ActionCompletionVerifier,
|
||||
cookies: Map<String, String>,
|
||||
script: String?,
|
||||
url: String,
|
||||
autoSolveSubmitBtnSelector: String? = null) {
|
||||
fun launchCaptcha(
|
||||
context: Context,
|
||||
source: ActionCompletionVerifier,
|
||||
cookies: Map<String, String>,
|
||||
script: String?,
|
||||
url: String,
|
||||
autoSolveSubmitBtnSelector: String? = null
|
||||
) {
|
||||
val intent = baseIntent(context).apply {
|
||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
||||
@ -641,9 +646,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun launchUniversal(context: Context,
|
||||
source: HttpSource,
|
||||
url: String) {
|
||||
fun launchUniversal(
|
||||
context: Context,
|
||||
source: HttpSource,
|
||||
url: String
|
||||
) {
|
||||
val intent = baseIntent(context).apply {
|
||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||
putExtra(URL_EXTRA, url)
|
||||
@ -652,9 +659,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun launchUniversal(context: Context,
|
||||
sourceId: Long,
|
||||
url: String) {
|
||||
fun launchUniversal(
|
||||
context: Context,
|
||||
sourceId: Long,
|
||||
url: String
|
||||
) {
|
||||
val intent = baseIntent(context).apply {
|
||||
putExtra(SOURCE_ID_EXTRA, sourceId)
|
||||
putExtra(URL_EXTRA, url)
|
||||
@ -663,11 +672,13 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun launchAction(context: Context,
|
||||
completionVerifier: ActionCompletionVerifier,
|
||||
script: String?,
|
||||
url: String,
|
||||
actionName: String) {
|
||||
fun launchAction(
|
||||
context: Context,
|
||||
completionVerifier: ActionCompletionVerifier,
|
||||
script: String?,
|
||||
url: String,
|
||||
actionName: String
|
||||
) {
|
||||
val intent = baseIntent(context).apply {
|
||||
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
|
||||
putExtra(SCRIPT_EXTRA, script)
|
||||
@ -678,12 +689,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun launchAction(context: Context,
|
||||
completionVerifier: (String) -> Boolean,
|
||||
script: String?,
|
||||
url: String,
|
||||
actionName: String,
|
||||
headers: Map<String, String>? = emptyMap()) {
|
||||
fun launchAction(
|
||||
context: Context,
|
||||
completionVerifier: (String) -> Boolean,
|
||||
script: String?,
|
||||
url: String,
|
||||
actionName: String,
|
||||
headers: Map<String, String>? = emptyMap()
|
||||
) {
|
||||
val intent = baseIntent(context).apply {
|
||||
putExtra(HEADERS_EXTRA, HashMap(headers))
|
||||
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 {
|
||||
override val versionId get() = source.versionId
|
||||
override val lang: String get() = source.lang
|
||||
@ -708,4 +721,3 @@ class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHtt
|
||||
interface ActionCompletionVerifier : Source {
|
||||
fun verifyComplete(url: String): Boolean
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,13 @@ import android.webkit.WebView
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
open class HeadersInjectingWebViewClient(activity: BrowserActionActivity,
|
||||
verifyComplete: (String) -> Boolean,
|
||||
injectScript: String?,
|
||||
private val headers: Map<String, String>)
|
||||
: BasicWebViewClient(activity, verifyComplete, injectScript) {
|
||||
open class HeadersInjectingWebViewClient(
|
||||
activity: BrowserActionActivity,
|
||||
verifyComplete: (String) -> Boolean,
|
||||
injectScript: String?,
|
||||
private val headers: Map<String, String>
|
||||
) :
|
||||
BasicWebViewClient(activity, verifyComplete, injectScript) {
|
||||
|
||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||
// Temp disabled as it's unreliable
|
||||
|
@ -23,7 +23,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.eh_activity_intercept)
|
||||
|
||||
//Show back button
|
||||
// Show back button
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
@ -31,7 +31,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
||||
}
|
||||
|
||||
private fun processLink() {
|
||||
if(Intent.ACTION_VIEW == intent.action) {
|
||||
if (Intent.ACTION_VIEW == intent.action) {
|
||||
intercept_progress.visible()
|
||||
intercept_status.text = "Loading gallery..."
|
||||
presenter.loadGallery(intent.dataString)
|
||||
@ -52,7 +52,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
||||
statusSubscription = presenter.status
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when(it) {
|
||||
when (it) {
|
||||
is InterceptResult.Success -> {
|
||||
intercept_progress.gone()
|
||||
intercept_status.text = "Launching app..."
|
||||
|
@ -3,8 +3,8 @@ package exh.ui.intercept
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import exh.GalleryAddEvent
|
||||
import exh.GalleryAdder
|
||||
import rx.subjects.BehaviorSubject
|
||||
import kotlin.concurrent.thread
|
||||
import rx.subjects.BehaviorSubject
|
||||
|
||||
class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||
private val galleryAdder = GalleryAdder()
|
||||
@ -13,11 +13,11 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||
|
||||
@Synchronized
|
||||
fun loadGallery(gallery: String) {
|
||||
//Do not load gallery if already loading
|
||||
if(status.value is InterceptResult.Idle) {
|
||||
// Do not load gallery if already loading
|
||||
if (status.value is InterceptResult.Idle) {
|
||||
status.onNext(InterceptResult.Loading())
|
||||
|
||||
//Load gallery async
|
||||
// Load gallery async
|
||||
thread {
|
||||
val result = galleryAdder.addGallery(gallery)
|
||||
|
||||
@ -35,6 +35,6 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||
sealed class InterceptResult {
|
||||
class Idle : InterceptResult()
|
||||
class Loading : InterceptResult()
|
||||
data class Success(val mangaId: Long): InterceptResult()
|
||||
data class Failure(val reason: String): InterceptResult()
|
||||
data class Success(val mangaId: Long) : InterceptResult()
|
||||
data class Failure(val reason: String) : InterceptResult()
|
||||
}
|
@ -28,21 +28,21 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
val fingerprintSupported
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& Reprint.isHardwarePresent()
|
||||
&& Reprint.hasFingerprintRegistered()
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered()
|
||||
|
||||
val useFingerprint
|
||||
get() = fingerprintSupported
|
||||
&& prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
get() = fingerprintSupported &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
if(fingerprintSupported) {
|
||||
if (fingerprintSupported) {
|
||||
updateSummary()
|
||||
onChange {
|
||||
if(it as Boolean)
|
||||
if (it as Boolean)
|
||||
tryChange()
|
||||
else
|
||||
prefs.eh_lockUseFingerprint().set(false)
|
||||
@ -51,7 +51,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
} else {
|
||||
title = "Fingerprint unsupported"
|
||||
shouldDisableView = true
|
||||
summary = if(!Reprint.hasFingerprintRegistered())
|
||||
summary = if (!Reprint.hasFingerprintRegistered())
|
||||
"No fingerprints enrolled!"
|
||||
else
|
||||
"Fingerprint unlock is unsupported on this device!"
|
||||
@ -61,7 +61,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
||||
|
||||
private fun updateSummary() {
|
||||
isChecked = useFingerprint
|
||||
title = if(isChecked)
|
||||
title = if (isChecked)
|
||||
"Fingerprint enabled"
|
||||
else
|
||||
"Fingerprint disabled"
|
||||
|
@ -1,6 +1,5 @@
|
||||
package exh.ui.lock
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.WindowManager
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
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.getOrDefault
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
object LockActivityDelegate {
|
||||
private val preferences by injectLazy<PreferencesHelper>()
|
||||
@ -20,7 +18,6 @@ object LockActivityDelegate {
|
||||
.popChangeHandler(LockChangeHandler(animate)))
|
||||
}
|
||||
|
||||
|
||||
fun onCreate(activity: FragmentActivity) {
|
||||
preferences.secureScreen().asObservable()
|
||||
.subscribe {
|
||||
@ -42,5 +39,4 @@ object LockActivityDelegate {
|
||||
private fun isAppLocked(router: Router): Boolean {
|
||||
return router.backstack.lastOrNull()?.controller() is LockController
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
||||
import java.util.*
|
||||
import java.util.ArrayList
|
||||
|
||||
class LockChangeHandler : AnimatorChangeHandler {
|
||||
constructor(): super()
|
||||
constructor() : super()
|
||||
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
|
||||
@ -36,6 +36,4 @@ class LockChangeHandler : AnimatorChangeHandler {
|
||||
|
||||
override fun copy(): ControllerChangeHandler =
|
||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
|
||||
}
|
||||
|
||||
|
@ -22,8 +22,8 @@ class LockController : NucleusController<LockPresenter>() {
|
||||
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup)
|
||||
= inflater.inflate(R.layout.activity_lock, container, false)!!
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup) =
|
||||
inflater.inflate(R.layout.activity_lock, container, false)!!
|
||||
|
||||
override fun createPresenter() = LockPresenter()
|
||||
|
||||
@ -32,13 +32,13 @@ class LockController : NucleusController<LockPresenter>() {
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
if(!lockEnabled(prefs)) {
|
||||
if (!lockEnabled(prefs)) {
|
||||
closeLock()
|
||||
return
|
||||
}
|
||||
|
||||
with(view) {
|
||||
//Setup pin lock
|
||||
// Setup pin lock
|
||||
pin_lock_view.attachIndicatorDots(indicator_dots)
|
||||
|
||||
pin_lock_view.pinLength = prefs.eh_lockLength().getOrDefault()
|
||||
@ -47,7 +47,7 @@ class LockController : NucleusController<LockPresenter>() {
|
||||
|
||||
override fun onComplete(pin: String) {
|
||||
if (sha512(pin, prefs.eh_lockSalt().get()!!) == prefs.eh_lockHash().get()) {
|
||||
//Yay!
|
||||
// Yay!
|
||||
closeLock()
|
||||
} else {
|
||||
MaterialDialog.Builder(context)
|
||||
@ -72,7 +72,7 @@ class LockController : NucleusController<LockPresenter>() {
|
||||
super.onAttach(view)
|
||||
|
||||
with(view) {
|
||||
//Fingerprint
|
||||
// Fingerprint
|
||||
if (presenter.useFingerprint) {
|
||||
swirl_container.visibility = View.VISIBLE
|
||||
swirl_container.removeAllViews()
|
||||
@ -90,7 +90,7 @@ class LockController : NucleusController<LockPresenter>() {
|
||||
val lockColor = resolvColor(android.R.attr.windowBackground)
|
||||
setBackgroundColor(lockColor)
|
||||
val bgColor = resolvColor(android.R.attr.colorBackground)
|
||||
//Disable elevation if lock color is same as background color
|
||||
// Disable elevation if lock color is same as background color
|
||||
if (lockColor == bgColor)
|
||||
this@with.swirl_container.cardElevation = 0f
|
||||
setState(SwirlView.State.OFF, true)
|
||||
|
@ -8,12 +8,12 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
import java.math.BigInteger
|
||||
import java.security.SecureRandom
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.math.BigInteger
|
||||
import java.security.SecureRandom
|
||||
|
||||
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
SwitchPreferenceCompat(context, attrs) {
|
||||
@ -33,7 +33,7 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
|
||||
private fun updateSummary() {
|
||||
isChecked = lockEnabled(prefs)
|
||||
if(isChecked) {
|
||||
if (isChecked) {
|
||||
title = "Lock enabled"
|
||||
summary = "Tap to disable or change pin code"
|
||||
} else {
|
||||
@ -43,7 +43,7 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
}
|
||||
|
||||
fun tryChange() {
|
||||
if(!notifyLockSecurity(context)) {
|
||||
if (!notifyLockSecurity(context)) {
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Lock application")
|
||||
.content("Enter a pin to lock the application. Enter nothing to disable the pin lock.")
|
||||
@ -76,7 +76,7 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
val salt: String?
|
||||
val hash: String?
|
||||
val length: Int
|
||||
if(password.isEmpty()) {
|
||||
if (password.isEmpty()) {
|
||||
salt = null
|
||||
hash = null
|
||||
length = -1
|
||||
|
@ -7,13 +7,12 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class LockPresenter: BasePresenter<LockController>() {
|
||||
class LockPresenter : BasePresenter<LockController>() {
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
val useFingerprint
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& Reprint.isHardwarePresent()
|
||||
&& Reprint.hasFingerprintRegistered()
|
||||
&& prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Reprint.isHardwarePresent() &&
|
||||
Reprint.hasFingerprintRegistered() &&
|
||||
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||
}
|
||||
|
||||
|
@ -13,11 +13,10 @@ import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.security.MessageDigest
|
||||
import kotlin.experimental.and
|
||||
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Password hashing utils
|
||||
@ -40,22 +39,24 @@ fun sha512(passwordToHash: String, salt: String): String {
|
||||
/**
|
||||
* Check if lock is enabled
|
||||
*/
|
||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get())
|
||||
= prefs.eh_lockHash().get() != null
|
||||
&& prefs.eh_lockSalt().get() != null
|
||||
&& prefs.eh_lockLength().getOrDefault() != -1
|
||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) =
|
||||
prefs.eh_lockHash().get() != null &&
|
||||
prefs.eh_lockSalt().get() != null &&
|
||||
prefs.eh_lockLength().getOrDefault() != -1
|
||||
|
||||
/**
|
||||
* Check if the lock will function properly
|
||||
*
|
||||
* @return true if action is required, false if lock is working properly
|
||||
*/
|
||||
fun notifyLockSecurity(context: Context,
|
||||
prefs: PreferencesHelper = Injekt.get()): Boolean {
|
||||
fun notifyLockSecurity(
|
||||
context: Context,
|
||||
prefs: PreferencesHelper = Injekt.get()
|
||||
): Boolean {
|
||||
return false
|
||||
if (!prefs.eh_lockManually().getOrDefault()
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& !hasAccessToUsageStats(context)) {
|
||||
if (!prefs.eh_lockManually().getOrDefault() &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
!hasAccessToUsageStats(context)) {
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Permission required")
|
||||
.content("${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
|
||||
@ -66,7 +67,7 @@ fun notifyLockSecurity(context: Context,
|
||||
.onPositive { _, _ ->
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} catch(e: ActivityNotFoundException) {
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
|
||||
MaterialDialog.Builder(context)
|
||||
.title("Grant permission manually")
|
||||
|
@ -12,14 +12,14 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import exh.uconfig.WarnConfigureDialogController
|
||||
import java.net.HttpCookie
|
||||
import kotlinx.android.synthetic.main.eh_activity_login.view.*
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.HttpCookie
|
||||
|
||||
/**
|
||||
* LoginController
|
||||
@ -104,17 +104,17 @@ class LoginController : NucleusController<LoginPresenter>() {
|
||||
Timber.d(url)
|
||||
val parsedUrl = Uri.parse(url)
|
||||
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
|
||||
//Hide distracting content
|
||||
if(!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
// Hide distracting content
|
||||
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT) &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
view.evaluateJavascript(HIDE_JS, null)
|
||||
|
||||
//Check login result
|
||||
// Check login result
|
||||
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
|
||||
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
|
||||
}
|
||||
} else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
|
||||
//At ExHentai, check that everything worked out...
|
||||
// At ExHentai, check that everything worked out...
|
||||
if (applyExHentaiCookies(url)) {
|
||||
preferenceManager.enableExhentai().set(true)
|
||||
finishLogin()
|
||||
@ -128,7 +128,7 @@ class LoginController : NucleusController<LoginPresenter>() {
|
||||
fun finishLogin() {
|
||||
router.popCurrentController()
|
||||
|
||||
//Upload settings
|
||||
// Upload settings
|
||||
WarnConfigureDialogController.uploadSettings(router)
|
||||
}
|
||||
|
||||
@ -138,9 +138,9 @@ class LoginController : NucleusController<LoginPresenter>() {
|
||||
fun checkLoginCookies(url: String): Boolean {
|
||||
getCookies(url)?.let { parsed ->
|
||||
return parsed.filter {
|
||||
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true)
|
||||
|| it.name.equals(PASS_HASH_COOKIE, ignoreCase = true))
|
||||
&& it.value.isNotBlank()
|
||||
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
|
||||
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)) &&
|
||||
it.value.isNotBlank()
|
||||
}.count() >= 2
|
||||
}
|
||||
return false
|
||||
@ -164,10 +164,10 @@ class LoginController : NucleusController<LoginPresenter>() {
|
||||
}
|
||||
}
|
||||
|
||||
//Missing a cookie
|
||||
// Missing a cookie
|
||||
if (memberId == null || passHash == null || igneous == null) return false
|
||||
|
||||
//Update prefs
|
||||
// Update prefs
|
||||
preferenceManager.memberIdVal().set(memberId)
|
||||
preferenceManager.passHashVal().set(passHash)
|
||||
preferenceManager.igneousVal().set(igneous)
|
||||
@ -177,8 +177,8 @@ class LoginController : NucleusController<LoginPresenter>() {
|
||||
return false
|
||||
}
|
||||
|
||||
fun getCookies(url: String): List<HttpCookie>?
|
||||
= CookieManager.getInstance().getCookie(url)?.let {
|
||||
fun getCookies(url: String): List<HttpCookie>? =
|
||||
CookieManager.getInstance().getCookie(url)?.let {
|
||||
it.split("; ").flatMap {
|
||||
HttpCookie.parse(it)
|
||||
}
|
||||
|
@ -2,6 +2,4 @@ package exh.ui.login
|
||||
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
|
||||
class LoginPresenter: BasePresenter<LoginController>() {
|
||||
|
||||
}
|
||||
class LoginPresenter : BasePresenter<LoginController>()
|
||||
|
@ -12,9 +12,9 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.isLewdSource
|
||||
import kotlin.concurrent.thread
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class MetadataFetchDialog {
|
||||
|
||||
@ -25,7 +25,7 @@ class MetadataFetchDialog {
|
||||
val preferenceHelper: PreferencesHelper by injectLazy()
|
||||
|
||||
fun show(context: Activity) {
|
||||
//Too lazy to actually deal with orientation changes
|
||||
// Too lazy to actually deal with orientation changes
|
||||
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
|
||||
var running = true
|
||||
@ -55,7 +55,7 @@ class MetadataFetchDialog {
|
||||
|
||||
val mangaWithMissingMetadata = libraryMangas
|
||||
.filterIndexed { index, libraryManga ->
|
||||
if(index % 100 == 0) {
|
||||
if (index % 100 == 0) {
|
||||
context.runOnUiThread {
|
||||
progressDialog.setContent("[Stage 1/2] Scanning for missing metadata...")
|
||||
progressDialog.setProgress(index + 1)
|
||||
@ -69,9 +69,9 @@ class MetadataFetchDialog {
|
||||
progressDialog.maxProgress = mangaWithMissingMetadata.size
|
||||
}
|
||||
|
||||
//Actual metadata fetch code
|
||||
for((i, manga) in mangaWithMissingMetadata.withIndex()) {
|
||||
if(!running) break
|
||||
// Actual metadata fetch code
|
||||
for ((i, manga) in mangaWithMissingMetadata.withIndex()) {
|
||||
if (!running) break
|
||||
context.runOnUiThread {
|
||||
progressDialog.setContent("[Stage 2/2] Processing: ${manga.title}")
|
||||
progressDialog.setProgress(i + 1)
|
||||
@ -88,10 +88,10 @@ class MetadataFetchDialog {
|
||||
|
||||
context.runOnUiThread {
|
||||
// Ensure activity still exists before we do anything to the activity
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !context.isDestroyed) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !context.isDestroyed) {
|
||||
progressDialog.dismiss()
|
||||
|
||||
//Enable orientation changes again
|
||||
// Enable orientation changes again
|
||||
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
|
||||
if (running) displayMigrationComplete(context)
|
||||
@ -103,12 +103,12 @@ class MetadataFetchDialog {
|
||||
fun askMigration(activity: Activity, explicit: Boolean) {
|
||||
var extra = ""
|
||||
db.getLibraryMangas().asRxSingle().subscribe {
|
||||
if(!explicit && it.none { isLewdSource(it.source) }) {
|
||||
if (!explicit && it.none { isLewdSource(it.source) }) {
|
||||
// Do not open dialog on startup if no manga
|
||||
// Also do not check again
|
||||
preferenceHelper.migrateLibraryAsked().set(true)
|
||||
} else {
|
||||
//Not logged in but have ExHentai galleries
|
||||
// Not logged in but have ExHentai galleries
|
||||
if (!preferenceHelper.enableExhentai().getOrDefault()) {
|
||||
it.find { it.source == EXH_SOURCE_ID }?.let {
|
||||
extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
|
||||
@ -132,7 +132,6 @@ class MetadataFetchDialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun adviseMigrationLater(activity: Activity) {
|
||||
|
@ -5,7 +5,7 @@ class MigrationStatus {
|
||||
val NOT_INITIALIZED = -1
|
||||
val COMPLETED = 0
|
||||
|
||||
//Migration process
|
||||
// Migration process
|
||||
val NOTIFY_USER = 1
|
||||
val OPEN_BACKUP_MENU = 2
|
||||
val PERFORM_BACKUP = 3
|
||||
|
@ -76,16 +76,16 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
|
||||
updateOptionsState()
|
||||
|
||||
begin_migration_btn.setOnClickListener {
|
||||
if(!showingOptions) {
|
||||
if (!showingOptions) {
|
||||
showingOptions = true
|
||||
updateOptionsState()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
var flags = 0
|
||||
if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||
if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||
if(mig_categories.isChecked) flags = flags or MigrationFlags.TRACK
|
||||
if (mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||
if (mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||
if (mig_categories.isChecked) flags = flags or MigrationFlags.TRACK
|
||||
|
||||
router.replaceTopController(MigrationProcedureController.create(
|
||||
MigrationProcedureConfig(
|
||||
@ -97,7 +97,7 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
|
||||
enableLenientSearch = use_smart_search.isChecked,
|
||||
migrationFlags = flags,
|
||||
copy = copy_manga.isChecked,
|
||||
extraSearchParams = if(extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) {
|
||||
extraSearchParams = if (extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) {
|
||||
extra_search_param_text.text.toString()
|
||||
} else null
|
||||
)
|
||||
@ -109,7 +109,7 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
|
||||
if (showingOptions) {
|
||||
begin_migration_btn.text = "Begin migration"
|
||||
options_group.visible()
|
||||
if(extra_search_param.isChecked) {
|
||||
if (extra_search_param.isChecked) {
|
||||
extra_search_param_text.visible()
|
||||
} else {
|
||||
extra_search_param_text.gone()
|
||||
@ -122,7 +122,7 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
|
||||
}
|
||||
|
||||
override fun handleBack(): Boolean {
|
||||
if(showingOptions) {
|
||||
if (showingOptions) {
|
||||
showingOptions = false
|
||||
updateOptionsState()
|
||||
return true
|
||||
@ -142,7 +142,7 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
|
||||
}
|
||||
|
||||
private fun updatePrioritizeChapterCount(migrationMode: Boolean) {
|
||||
migration_mode.text = if(migrationMode) {
|
||||
migration_mode.text = if (migrationMode) {
|
||||
"Currently using the source with the most chapters and the above list to break ties (slow with many sources or smart search)"
|
||||
} else {
|
||||
"Currently using the first source in the list that has the manga"
|
||||
|
@ -4,8 +4,10 @@ import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import exh.debug.DebugFunctions.sourceManager
|
||||
|
||||
class MigrationSourceAdapter(val items: List<MigrationSourceItem>,
|
||||
val controller: MigrationDesignController): FlexibleAdapter<MigrationSourceItem>(
|
||||
class MigrationSourceAdapter(
|
||||
val items: List<MigrationSourceItem>,
|
||||
val controller: MigrationDesignController
|
||||
) : FlexibleAdapter<MigrationSourceItem>(
|
||||
items,
|
||||
controller,
|
||||
true
|
||||
|
@ -1,14 +1,14 @@
|
||||
package exh.ui.migration.manga.design
|
||||
|
||||
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.view.getRound
|
||||
import kotlinx.android.synthetic.main.eh_source_item.*
|
||||
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
|
||||
|
||||
class MigrationSourceHolder(view: View, val adapter: FlexibleAdapter<MigrationSourceItem>):
|
||||
class MigrationSourceHolder(view: View, val adapter: FlexibleAdapter<MigrationSourceItem>) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
init {
|
||||
setDragHandleView(reorder)
|
||||
@ -20,10 +20,10 @@ class MigrationSourceHolder(view: View, val adapter: FlexibleAdapter<MigrationSo
|
||||
|
||||
// Update circle letter image.
|
||||
itemView.post {
|
||||
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
|
||||
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(), false))
|
||||
}
|
||||
|
||||
if(sourceEnabled) {
|
||||
if (sourceEnabled) {
|
||||
title.alpha = 1.0f
|
||||
image.alpha = 1.0f
|
||||
title.paintFlags = title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
|
||||
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): AbstractFlexibleItem<MigrationSourceHolder>() {
|
||||
class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean) : AbstractFlexibleItem<MigrationSourceHolder>() {
|
||||
override fun getLayoutRes() = R.layout.eh_source_item
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): MigrationSourceHolder {
|
||||
@ -25,10 +25,12 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): A
|
||||
* @param position The position of this item in the adapter.
|
||||
* @param payloads List of partial changes.
|
||||
*/
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
|
||||
holder: MigrationSourceHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
|
||||
holder: MigrationSourceHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(source, sourceEnabled)
|
||||
}
|
||||
|
||||
@ -52,7 +54,7 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): A
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ParcelableSI(val sourceId: Long, val sourceEnabled: Boolean): Parcelable
|
||||
data class ParcelableSI(val sourceId: Long, val sourceEnabled: Boolean) : Parcelable
|
||||
|
||||
fun asParcelable(): ParcelableSI {
|
||||
return ParcelableSI(source.id, sourceEnabled)
|
||||
|
@ -5,8 +5,8 @@ import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
|
||||
class DeactivatableViewPager : androidx.viewpager.widget.ViewPager {
|
||||
constructor(context: Context): super(context)
|
||||
constructor(context: Context, attrs: AttributeSet): super(context, attrs)
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return !isEnabled || super.onTouchEvent(event)
|
||||
|
@ -6,14 +6,17 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import exh.util.DeferredField
|
||||
import exh.util.await
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
|
||||
class MigratingManga(private val db: DatabaseHelper,
|
||||
private val sourceManager: SourceManager,
|
||||
val mangaId: Long,
|
||||
parentContext: CoroutineContext) {
|
||||
class MigratingManga(
|
||||
private val db: DatabaseHelper,
|
||||
private val sourceManager: SourceManager,
|
||||
val mangaId: Long,
|
||||
parentContext: CoroutineContext
|
||||
) {
|
||||
val searchResult = DeferredField<Long?>()
|
||||
|
||||
// <MAX, PROGRESS>
|
||||
@ -24,7 +27,7 @@ class MigratingManga(private val db: DatabaseHelper,
|
||||
@Volatile
|
||||
private var manga: Manga? = null
|
||||
suspend fun manga(): Manga? {
|
||||
if(manga == null) manga = db.getManga(mangaId).await()
|
||||
if (manga == null) manga = db.getManga(mangaId).await()
|
||||
return manga
|
||||
}
|
||||
|
||||
|
@ -22,20 +22,27 @@ import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.util.await
|
||||
import kotlinx.android.synthetic.main.eh_manga_card.view.*
|
||||
import kotlinx.android.synthetic.main.eh_migration_process_item.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.android.synthetic.main.eh_manga_card.view.*
|
||||
import kotlinx.android.synthetic.main.eh_migration_process_item.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
val migratingManga: List<MigratingManga>,
|
||||
override val coroutineContext: CoroutineContext) : androidx.viewpager.widget.PagerAdapter(), CoroutineScope {
|
||||
class MigrationProcedureAdapter(
|
||||
val controller: MigrationProcedureController,
|
||||
val migratingManga: List<MigratingManga>,
|
||||
override val coroutineContext: CoroutineContext
|
||||
) : androidx.viewpager.widget.PagerAdapter(), CoroutineScope {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val gson: Gson by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
@ -69,7 +76,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
performMigration(item)
|
||||
}
|
||||
controller.nextMigration()
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
logger.e("Migration failure!", e)
|
||||
controller.migrationFailure()
|
||||
}
|
||||
@ -81,7 +88,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
}
|
||||
|
||||
suspend fun performMigration(manga: MigratingManga) {
|
||||
if(!manga.searchResult.initialized) {
|
||||
if (!manga.searchResult.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -96,9 +103,11 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateMangaInternal(prevManga: Manga,
|
||||
manga: Manga,
|
||||
replace: Boolean) {
|
||||
private fun migrateMangaInternal(
|
||||
prevManga: Manga,
|
||||
manga: Manga,
|
||||
replace: Boolean
|
||||
) {
|
||||
db.inTransaction {
|
||||
// Update chapters read
|
||||
if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
|
||||
@ -147,7 +156,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
tag.launch {
|
||||
val manga = migratingManga.manga()
|
||||
val source = migratingManga.mangaSource()
|
||||
if(manga != null) {
|
||||
if (manga != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
eh_manga_card_from.loading_group.gone()
|
||||
eh_manga_card_from.attachManga(tag, manga, source)
|
||||
@ -174,7 +183,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
sourceManager.get(it)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if(searchResult != null && resultSource != null) {
|
||||
if (searchResult != null && resultSource != null) {
|
||||
eh_manga_card_to.loading_group.gone()
|
||||
eh_manga_card_to.attachManga(tag, searchResult, resultSource)
|
||||
eh_manga_card_to.setOnClickListener {
|
||||
@ -263,7 +272,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
(objectAsView.tag as? ViewTag)?.destroy()
|
||||
}
|
||||
|
||||
class ViewTag(parent: CoroutineContext): CoroutineScope {
|
||||
class ViewTag(parent: CoroutineContext) : CoroutineScope {
|
||||
/**
|
||||
* The context of this scope.
|
||||
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
|
||||
|
@ -5,11 +5,11 @@ import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MigrationProcedureConfig(
|
||||
val mangaIds: List<Long>,
|
||||
val targetSourceIds: List<Long>,
|
||||
val useSourceWithMostChapters: Boolean,
|
||||
val enableLenientSearch: Boolean,
|
||||
val migrationFlags: Int,
|
||||
val copy: Boolean,
|
||||
val extraSearchParams: String?
|
||||
): Parcelable
|
||||
val mangaIds: List<Long>,
|
||||
val targetSourceIds: List<Long>,
|
||||
val useSourceWithMostChapters: Boolean,
|
||||
val enableLenientSearch: Boolean,
|
||||
val migrationFlags: Int,
|
||||
val copy: Boolean,
|
||||
val extraSearchParams: String?
|
||||
) : Parcelable
|
||||
|
@ -15,13 +15,21 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.smartsearch.SmartSearchEngine
|
||||
import exh.ui.base.BaseExhController
|
||||
import exh.util.await
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlinx.android.synthetic.main.eh_migration_process.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
// TODO Will probably implode if activity is fully destroyed
|
||||
class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(bundle), CoroutineScope {
|
||||
@ -70,7 +78,7 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
pager.adapter = adapter
|
||||
pager.isEnabled = false
|
||||
|
||||
if(migrationsJob == null) {
|
||||
if (migrationsJob == null) {
|
||||
migrationsJob = launch {
|
||||
runMigrations(newMigratingManga)
|
||||
}
|
||||
@ -89,7 +97,7 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
|
||||
fun nextMigration() {
|
||||
adapter?.let { adapter ->
|
||||
if(pager.currentItem >= adapter.count - 1) {
|
||||
if (pager.currentItem >= adapter.count - 1) {
|
||||
applicationContext?.toast("All migrations complete!")
|
||||
router.popCurrentController()
|
||||
} else {
|
||||
@ -115,11 +123,11 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
suspend fun runMigrations(mangas: List<MigratingManga>) {
|
||||
val sources = config.targetSourceIds.mapNotNull { sourceManager.get(it) as? CatalogueSource }
|
||||
|
||||
for(manga in mangas) {
|
||||
if(!manga.searchResult.initialized && manga.migrationJob.isActive) {
|
||||
for (manga in mangas) {
|
||||
if (!manga.searchResult.initialized && manga.migrationJob.isActive) {
|
||||
val mangaObj = manga.manga()
|
||||
|
||||
if(mangaObj == null) {
|
||||
if (mangaObj == null) {
|
||||
manga.searchResult.initialize(null)
|
||||
continue
|
||||
}
|
||||
@ -131,39 +139,39 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
val validSources = sources.filter {
|
||||
it.id != mangaSource.id
|
||||
}
|
||||
if(config.useSourceWithMostChapters) {
|
||||
if (config.useSourceWithMostChapters) {
|
||||
val sourceSemaphore = Semaphore(3)
|
||||
val processedSources = AtomicInteger()
|
||||
|
||||
validSources.map { source ->
|
||||
async {
|
||||
sourceSemaphore.withPermit {
|
||||
try {
|
||||
val searchResult = if (config.enableLenientSearch) {
|
||||
smartSearchEngine.smartSearch(source, mangaObj.title)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(source, mangaObj.title)
|
||||
}
|
||||
async {
|
||||
sourceSemaphore.withPermit {
|
||||
try {
|
||||
val searchResult = if (config.enableLenientSearch) {
|
||||
smartSearchEngine.smartSearch(source, mangaObj.title)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(source, mangaObj.title)
|
||||
}
|
||||
|
||||
if(searchResult != null) {
|
||||
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
|
||||
val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io())
|
||||
withContext(Dispatchers.IO) {
|
||||
syncChaptersWithSource(db, chapters, localManga, source)
|
||||
}
|
||||
manga.progress.send(validSources.size to processedSources.incrementAndGet())
|
||||
localManga to chapters.size
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch(e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch(e: Exception) {
|
||||
logger.e("Failed to search in source: ${source.id}!", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
if (searchResult != null) {
|
||||
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
|
||||
val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io())
|
||||
withContext(Dispatchers.IO) {
|
||||
syncChaptersWithSource(db, chapters, localManga, source)
|
||||
}
|
||||
manga.progress.send(validSources.size to processedSources.incrementAndGet())
|
||||
localManga to chapters.size
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
logger.e("Failed to search in source: ${source.id}!", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}.mapNotNull { it.await() }.maxBy { it.second }?.first
|
||||
} else {
|
||||
@ -183,28 +191,28 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
}
|
||||
localManga
|
||||
} else null
|
||||
} catch(e: CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
logger.e("Failed to search in source: ${source.id}!", e)
|
||||
null
|
||||
}
|
||||
|
||||
manga.progress.send(validSources.size to (index + 1))
|
||||
|
||||
if(searchResult != null) return@async searchResult
|
||||
if (searchResult != null) return@async searchResult
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}.await()
|
||||
} catch(e: CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore canceled migrations
|
||||
continue
|
||||
}
|
||||
|
||||
if(result != null && result.thumbnail_url == null) {
|
||||
if (result != null && result.thumbnail_url == null) {
|
||||
try {
|
||||
val newManga = sourceManager.getOrStub(result.source)
|
||||
.fetchMangaDetails(result)
|
||||
@ -213,10 +221,10 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
|
||||
result.copyFrom(newManga)
|
||||
|
||||
db.insertManga(result).await()
|
||||
} catch(e: CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
logger.e("Could not load search manga details", e)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,13 @@ import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.android.synthetic.main.eh_smart_search.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSearchPresenter>(), CoroutineScope {
|
||||
@ -37,7 +43,7 @@ class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSea
|
||||
|
||||
appbar.bringToFront()
|
||||
|
||||
if(source == null || smartSearchConfig == null) {
|
||||
if (source == null || smartSearchConfig == null) {
|
||||
router.popCurrentController()
|
||||
applicationContext?.toast("Missing data!")
|
||||
return
|
||||
@ -47,7 +53,7 @@ class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSea
|
||||
presenter
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
for(event in presenter.smartSearchChannel) {
|
||||
for (event in presenter.smartSearchChannel) {
|
||||
withContext(NonCancellable) {
|
||||
if (event is SmartSearchPresenter.SearchResults.Found) {
|
||||
val transaction = MangaController(event.manga, true, smartSearchConfig).withFadeTransaction()
|
||||
|
@ -8,10 +8,15 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import exh.smartsearch.SmartSearchEngine
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: CatalogueController.SmartSearchConfig?):
|
||||
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: CatalogueController.SmartSearchConfig?) :
|
||||
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||
private val logger = XLog.tag("SmartSearchPresenter")
|
||||
|
||||
@ -24,7 +29,7 @@ class SmartSearchPresenter(private val source: CatalogueSource?, private val con
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
if(source != null && config != null) {
|
||||
if (source != null && config != null) {
|
||||
launch(Dispatchers.Default) {
|
||||
val result = try {
|
||||
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
||||
@ -48,7 +53,6 @@ class SmartSearchPresenter(private val source: CatalogueSource?, private val con
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
@ -58,8 +62,8 @@ class SmartSearchPresenter(private val source: CatalogueSource?, private val con
|
||||
data class SearchEntry(val manga: SManga, val dist: Double)
|
||||
|
||||
sealed class SearchResults {
|
||||
data class Found(val manga: Manga): SearchResults()
|
||||
object NotFound: SearchResults()
|
||||
object Error: SearchResults()
|
||||
data class Found(val manga: Manga) : SearchResults()
|
||||
object NotFound : SearchResults()
|
||||
object Error : SearchResults()
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ class CachedField<T>(private val expiresAfterMs: Long) {
|
||||
|
||||
suspend fun obtain(producer: suspend () -> T): T {
|
||||
return mutex.withLock {
|
||||
if(initTime < 0 || System.currentTimeMillis() - initTime > expiresAfterMs) {
|
||||
if (initTime < 0 || System.currentTimeMillis() - initTime > expiresAfterMs) {
|
||||
content = producer()
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user