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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,18 +17,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,6 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")
}
companion object {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("%", "\\%")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,4 @@ package exh.ui.login
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
class LoginPresenter: BasePresenter<LoginController>() {
}
class LoginPresenter : BasePresenter<LoginController>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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