Add MangaDex only implementation of Mangadex Follows list

Add login dialog that pops up whenever you are not logged in when trying to browse MangaDex
Remove attempts at porting over chapter read history from older galleries to new ones
Disable latest for ExHentai, it was browse without buttons anyway
This commit is contained in:
Jobobby04
2020-09-11 23:12:13 -04:00
parent 8928aa77eb
commit b93298c411
63 changed files with 1492 additions and 163 deletions
@@ -42,7 +42,7 @@ class EHentaiUpdateHelper(context: Context) {
mangaIds.map { mangaId ->
Single.zip(
db.getManga(mangaId).asRxSingle(),
db.getChaptersByMangaId(mangaId).asRxSingle()
db.getChapters(mangaId).asRxSingle()
) { manga, chapters ->
ChapterChain(manga, chapters)
}.toObservable().filter {
@@ -152,7 +152,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
return@mapNotNull null
}
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minByOrNull {
val chapter = db.getChapters(manga.id!!).asRxSingle().await().minByOrNull {
it.date_upload
}
@@ -0,0 +1,56 @@
package exh.md
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import exh.md.follows.MangaDexFollowsController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.singleOrNull
import reactivecircus.flowbinding.android.view.clicks
class MangaDexFabHeaderAdapter(val controller: Controller, val source: CatalogueSource) :
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
private lateinit var binding: SourceFilterMangadexHeaderBinding
private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
binding = SourceFilterMangadexHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SavedSearchesViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
holder.bind()
}
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
binding.mangadexFollows.clicks()
.onEach {
controller.router.replaceTopController(MangaDexFollowsController(source).withFadeTransaction())
}
.launchIn(scope)
binding.mangadexRandom.clicks()
.onEach {
(source as? RandomMangaSource)?.fetchRandomMangaUrl()?.singleOrNull()?.let { randomMangaId ->
controller.router.replaceTopController(BrowseSourceController(source, randomMangaId).withFadeTransaction())
}
}
.launchIn(scope)
}
}
}
@@ -0,0 +1,39 @@
package exh.md.follows
import android.os.Bundle
import android.view.Menu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/
class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) {
constructor(source: CatalogueSource) : this(
Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
}
)
override fun getTitle(): String? {
return view?.context?.getString(R.string.mangadex_follows)
}
override fun createPresenter(): BrowseSourcePresenter {
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_open_in_web_view).isVisible = false
menu.findItem(R.id.action_settings).isVisible = false
}
override fun initFilterSheet() {
// No-op: we don't allow filtering in latest
}
}
@@ -0,0 +1,21 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/**
* LatestUpdatesPager inherited from the general Pager.
*/
class MangaDexFollowsPager(val source: MangaDex) : Pager() {
override fun requestNext(): Observable<MangasPage> {
return source.fetchFollows()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { onPageReceived(it) }
}
}
@@ -0,0 +1,18 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import exh.source.EnhancedHttpSource
/**
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
val sourceAsMangaDex = (source as EnhancedHttpSource).enhancedSource as MangaDex
return MangaDexFollowsPager(sourceAsMangaDex)
}
}
@@ -17,8 +17,8 @@ import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.floor
import java.util.Date
import kotlin.math.floor
import okhttp3.Response
import rx.Completable
import rx.Single
@@ -41,7 +41,7 @@ class ApiMangaParser(private val langs: List<String>) {
*
* Will also save the metadata to the DB if possible
*/
fun parseToManga(manga: SManga, input: Response): Completable {
fun parseToManga(manga: SManga, input: Response, forceLatestCover: Boolean): Completable {
val mangaId = (manga as? Manga)?.id
val metaObservable = if (mangaId != null) {
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
@@ -55,7 +55,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
return metaObservable.map {
parseIntoMetadata(it, input)
parseIntoMetadata(it, input, forceLatestCover)
it.copyTo(manga)
it
}.flatMapCompletable {
@@ -66,7 +66,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
}
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) {
with(metadata) {
try {
val networkApiManga = MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), input.body!!.string())
@@ -74,15 +74,18 @@ class ApiMangaParser(private val langs: List<String>) {
mdId = MdUtil.getMangaId(input.request.url.toString())
mdUrl = input.request.url.toString()
title = MdUtil.cleanString(networkManga.title)
thumbnail_url = MdUtil.cdnUrl + MdUtil.removeTimeParamUrl(networkManga.cover_url)
val coverList = networkManga.covers
thumbnail_url = MdUtil.cdnUrl +
if (forceLatestCover && coverList.isNotEmpty()) {
coverList.last()
} else {
MdUtil.removeTimeParamUrl(networkManga.cover_url)
}
description = MdUtil.cleanDescription(networkManga.description)
author = MdUtil.cleanString(networkManga.author)
artist = MdUtil.cleanString(networkManga.artist)
lang_flag = networkManga.lang_flag
val lastChapter = networkManga.last_chapter?.toFloatOrNull()
lastChapter?.let {
last_chapter_number = floor(it).toInt()
}
last_chapter_number = networkManga.last_chapter?.toFloatOrNull()?.floor()
networkManga.rating?.let {
rating = it.bayesian ?: it.mean
@@ -107,10 +110,16 @@ class ApiMangaParser(private val langs: List<String>) {
status = tempStatus
}
val demographic = FilterHandler.demographics().filter { it.id == networkManga.demographic }.firstOrNull()
val genres =
networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] }
.toMutableList()
if (demographic != null) {
genres.add(0, demographic.name)
}
if (networkManga.hentai == 1) {
genres.add("Hentai")
}
@@ -135,7 +144,9 @@ class ApiMangaParser(private val langs: List<String>) {
if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) {
return false
}
val finalChapterNumber = serializer.manga.last_chapter!!
// just to fix the stupid lint
val lastMangaChapter: String? = serializer.manga.last_chapter
val finalChapterNumber = lastMangaChapter!!
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
filteredChapters.firstOrNull()?.let {
if (isOneShot(it.value, finalChapterNumber)) {
@@ -144,7 +155,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
}
val removeOneshots = filteredChapters.filter { !it.value.chapter.isNullOrBlank() }
return removeOneshots.size.toString() == floor(finalChapterNumber.toDouble()).toInt().toString()
return removeOneshots.size.toString() == finalChapterNumber.toDouble().floor().toString()
}
private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<Map.Entry<String, ChapterSerializer>> {
@@ -269,7 +280,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
if ((status == 2 || status == 3)) {
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
networkChapter.chapter == finalChapterNumber
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
) {
chapterName.add("[END]")
}
@@ -1,26 +0,0 @@
package exh.md.handlers
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.handlers.serializers.CoversResult
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
// Unused, look into what its used for todo
class CoverHandler(val client: OkHttpClient, val headers: Headers) {
suspend fun getCovers(manga: SManga): List<String> {
return withContext(Dispatchers.IO) {
val response = client.newCall(GET("${MdUtil.baseUrl}${MdUtil.coversApi}${MdUtil.getMangaId(manga.url)}", headers, CacheControl.FORCE_NETWORK)).execute()
val result = MdUtil.jsonParser.decodeFromString(
CoversResult.serializer(),
response.body!!.string()
)
result.covers.map { "${MdUtil.baseUrl}$it" }
}
}
}
@@ -171,7 +171,8 @@ class FilterHandler {
Tag("81", "Virtual Reality"),
Tag("82", "Zombies"),
Tag("83", "Incest"),
Tag("84", "Mafia")
Tag("84", "Mafia"),
Tag("85", "Villainess")
).sortedWith(compareBy { it.name })
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap()
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.handlers.serializers.FollowsPageResult
@@ -16,26 +17,24 @@ import exh.md.utils.MdUtil
import exh.md.utils.MdUtil.Companion.baseUrl
import exh.md.utils.MdUtil.Companion.getMangaId
import exh.metadata.metadata.MangaDexSearchMetadata
import kotlin.math.floor
import exh.util.floor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
// Unused, kept for future featues todo
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper) {
/**
* fetch follows by page
*/
fun fetchFollows(page: Int): Observable<MetadataMangasPage> {
return client.newCall(followsListRequest(page))
fun fetchFollows(): Observable<MangasPage> {
return client.newCall(followsListRequest())
.asObservable()
.map { response ->
followsParseMangaPage(response)
@@ -96,9 +95,9 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
val follow = result.first()
track.status = follow.follow_type
if (result[0].chapter.isNotBlank()) {
track.last_chapter_read = floor(follow.chapter.toFloat()).toInt()
track.last_chapter_read = follow.chapter.toFloat().floor()
}
track.tracking_url = MdUtil.baseUrl + follow.manga_id.toString()
track.tracking_url = baseUrl + follow.manga_id.toString()
track.title = follow.title
}
return track
@@ -107,11 +106,8 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**build Request for follows page
*
*/
private fun followsListRequest(page: Int): Request {
val url = "${MdUtil.baseUrl}${MdUtil.followsAllApi}".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
return GET(url.toString(), headers, CacheControl.FORCE_NETWORK)
private fun followsListRequest(): Request {
return GET("$baseUrl${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK)
}
/**
@@ -126,7 +122,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
title = MdUtil.cleanString(result.title)
mdUrl = "/manga/${result.manga_id}/"
thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers)
follow_status = FollowStatus.fromInt(result.follow_type)?.ordinal
follow_status = FollowStatus.fromInt(result.follow_type)?.int
}
}
@@ -205,21 +201,16 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**
* fetch all manga from all possible pages
*/
suspend fun fetchAllFollows(forceHd: Boolean): List<SManga> {
suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
return withContext(Dispatchers.IO) {
val listManga = mutableListOf<SManga>()
loop@ for (i in 1..10000) {
val response = client.newCall(followsListRequest(i))
.execute()
val mangasPage = followsParseMangaPage(response, forceHd)
if (mangasPage.mangas.isNotEmpty()) {
listManga.addAll(mangasPage.mangas)
val listManga = mutableListOf<Pair<SManga, MangaDexSearchMetadata>>()
val response = client.newCall(followsListRequest()).execute()
val mangasPage = followsParseMangaPage(response, forceHd)
listManga.addAll(
mangasPage.mangas.mapIndexed { index, sManga ->
sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
}
if (!mangasPage.hasNextPage) {
break@loop
}
}
)
listManga
}
}
@@ -227,7 +218,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
suspend fun fetchTrackingInfo(url: String): Track {
return withContext(Dispatchers.IO) {
val request = GET(
"${MdUtil.baseUrl}${MdUtil.followsMangaApi}" + getMangaId(url),
"$baseUrl${MdUtil.followsMangaApi}" + getMangaId(url),
headers,
CacheControl.FORCE_NETWORK
)
@@ -3,10 +3,13 @@ package exh.md.handlers
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.Headers
@@ -14,7 +17,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List<String>) {
class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List<String>, val forceLatestCovers: Boolean = false) {
// TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair<SManga, List<SChapter>> {
@@ -28,7 +31,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
throw Exception("Error from MangaDex Response code ${response.code} ")
}
parser.parseToManga(manga, response).await()
parser.parseToManga(manga, response, forceLatestCovers).await()
val chapterList = parser.chapterListParse(jsonData)
Pair(
manga,
@@ -48,7 +51,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
suspend fun fetchMangaDetails(manga: SManga): SManga {
return withContext(Dispatchers.IO) {
val response = client.newCall(apiRequest(manga)).execute()
ApiMangaParser(langs).parseToManga(manga, response).await()
ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers).await()
manga.apply {
initialized = true
}
@@ -59,7 +62,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
return client.newCall(apiRequest(manga))
.asObservableSuccess()
.flatMap {
ApiMangaParser(langs).parseToManga(manga, it).andThen(
ApiMangaParser(langs).parseToManga(manga, it, forceLatestCovers).andThen(
Observable.just(
manga.apply {
initialized = true
@@ -84,7 +87,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
}
}
fun fetchRandomMangaId(): Observable<String> {
fun fetchRandomMangaIdObservable(): Observable<String> {
return client.newCall(randomMangaRequest())
.asObservableSuccess()
.map { response ->
@@ -92,6 +95,13 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
}
}
fun fetchRandomMangaId(): Flow<String> {
return flow {
val response = client.newCall(randomMangaRequest()).await()
emit(ApiMangaParser(langs).randomMangaIdParse(response))
}
}
private fun randomMangaRequest(): Request {
return GET(MdUtil.baseUrl + MdUtil.randMangaPage)
}
@@ -42,7 +42,7 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers) {
val mangas = document.select(popularMangaSelector).map { element ->
popularMangaFromElement(element)
}.distinct()
}.distinctBy { it.url }
val hasNextPage = popularMangaNextPageSelector.let { selector ->
document.select(selector).first()
@@ -33,7 +33,7 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val
.map { response ->
val details = SManga.create()
details.url = "/manga/$realQuery/"
ApiMangaParser(langs).parseToManga(details, response).await()
ApiMangaParser(langs).parseToManga(details, response, preferences.mangaDexForceLatestCovers().get()).await()
MangasPage(listOf(details), false)
}
}
@@ -15,7 +15,9 @@ data class MangaSerializer(
val author: String,
val cover_url: String,
val description: String,
val demographic: String,
val genres: List<Int>,
val covers: List<String>,
val hentai: Int,
val lang_flag: String,
val lang_name: String,
+33 -3
View File
@@ -1,12 +1,17 @@
package exh.md.utils
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import exh.util.floor
import java.net.URI
import java.net.URISyntaxException
import kotlin.math.floor
import kotlinx.serialization.json.Json
import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MdUtil {
@@ -63,10 +68,15 @@ class MdUtil {
"[b][u]Spanish",
"[Espa&ntilde;ol]:",
"[b] Spanish: [/ b]",
"정보",
"Spanish/Espa&ntilde;ol",
"Espa&ntilde;ol / Spanish",
"Italian / Italiano",
"Italian/Italiano",
"\r\n\r\nItalian\r\n",
"Pasta-Pizza-Mandolino/Italiano",
"Persian /فارسی",
"Farsi/Persian/",
"Polish / polski",
"Polish / Polski",
"Polish Summary / Polski Opis",
@@ -89,6 +99,7 @@ class MdUtil {
"French - Français:",
"Turkish / T&uuml;rk&ccedil;e",
"Turkish/T&uuml;rk&ccedil;e",
"T&uuml;rk&ccedil;e",
"[b][u]Chinese",
"Arabic / العربية",
"العربية",
@@ -191,11 +202,11 @@ class MdUtil {
}.sortedByDescending { it.chapter_number }
remove0ChaptersFromCount.firstOrNull()?.let {
val chpNumber = floor(it.chapter_number).toInt()
val chpNumber = it.chapter_number.floor()
val allChapters = (1..chpNumber).toMutableSet()
remove0ChaptersFromCount.forEach {
allChapters.remove(floor(it.chapter_number).toInt())
allChapters.remove(it.chapter_number.floor())
}
if (allChapters.size <= 0) return null
@@ -203,6 +214,25 @@ class MdUtil {
}
return null
}
fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
val preferredMangaDexId = preferences.preferredMangaDexId().get().toLongOrNull()
mangadexs.firstOrNull { preferredMangaDexId != null && preferredMangaDexId != 0L && it.id == preferredMangaDexId } ?: mangadexs.firstOrNull()
}
}
fun getEnabledMangaDexs(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
val languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get()
return sourceManager.getDelegatedCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds }
.filterIsInstance(MangaDex::class.java)
}
fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/"
}
}
@@ -36,10 +36,5 @@ private const val EH_UNIVERSAL_INTERCEPTOR = -1L
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
EH_UNIVERSAL_INTERCEPTOR to listOf(
CAPTCHA_DETECTION_PATCH // Auto captcha detection
),
// MangaDex login support
*MANGADEX_SOURCE_IDS.map { id ->
id to listOf(MANGADEX_LOGIN_PATCH)
}.toTypedArray()
)
)
@@ -1,6 +1,7 @@
package exh.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@@ -234,4 +235,21 @@ class EnhancedHttpSource(
originalSource
}
}
companion object {
fun Source.getMainSource(): Source {
return if (this is EnhancedHttpSource) {
this.source()
} else {
this
}
}
fun Source.getOriginalSource(): Source {
return if (this is EnhancedHttpSource) {
this.originalSource
} else {
this
}
}
}
}
@@ -6,7 +6,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.databinding.MetadataViewItemBinding
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlin.math.floor
import exh.util.floor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -43,7 +43,7 @@ class MetadataViewAdapter(private var data: List<Pair<String, String>>) :
private var dataPosition: Int? = null
fun bind(position: Int) {
if (data.isEmpty() || !binding.infoText.text.isNullOrBlank()) return
dataPosition = floor(position / 2F).toInt()
dataPosition = (position / 2F).floor()
binding.infoText.text = if (position % 2 == 0) data[dataPosition!!].first else data[dataPosition!!].second
binding.infoText.clicks()
.onEach {
@@ -10,11 +10,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -73,9 +74,9 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
}
fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val thisSourceAsLewdSource = presenter.source.getMetadataSource()
if (thisSourceAsLewdSource != null) {
presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass)
val mainSource = presenter.source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(mainSource.metaClass)
}
}
+5
View File
@@ -0,0 +1,5 @@
package exh.util
fun Float.floor(): Int = kotlin.math.floor(this).toInt()
fun Double.floor(): Int = kotlin.math.floor(this).toInt()
@@ -0,0 +1,54 @@
package exh.widget.preference
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.isVisible
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.android.synthetic.main.pref_item_mangadex.view.*
class MangaDexLoginPreference @JvmOverloads constructor(
context: Context,
val source: MangaDex,
attrs: AttributeSet? = null
) : Preference(context, attrs) {
init {
layoutResource = R.layout.pref_item_mangadex
}
private var onLoginClick: () -> Unit = {}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.itemView.setOnClickListener {
onLoginClick()
}
val loginFrame = holder.itemView.login_frame
val color = if (source.isLogged()) {
context.getResourceColor(R.attr.colorAccent)
} else {
context.getResourceColor(R.attr.colorSecondary)
}
holder.itemView.login.setImageResource(R.drawable.ic_outline_people_alt_24dp)
holder.itemView.login.drawable.setTint(color)
loginFrame.isVisible = true
loginFrame.setOnClickListener {
onLoginClick()
}
}
fun setOnLoginClickListener(block: () -> Unit) {
onLoginClick = block
}
// Make method public
public override fun notifyChanged() {
super.notifyChanged()
}
}
@@ -0,0 +1,115 @@
package exh.widget.preference
import android.app.Activity
import android.app.Dialog
import android.os.Bundle
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference
import exh.md.utils.MdUtil
import kotlinx.android.synthetic.main.pref_account_login.view.login
import kotlinx.android.synthetic.main.pref_account_login.view.password
import kotlinx.android.synthetic.main.pref_account_login.view.username
import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangadexLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle = bundle) {
val source by lazy { MdUtil.getEnabledMangaDex() }
val service = Injekt.get<TrackManager>().mdList
val scope = CoroutineScope(Job() + Dispatchers.Main)
constructor(source: MangaDex, activity: Activity? = null) : this(
Bundle().apply {
putLong(
"key",
source.id
)
}
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply {
customView(R.layout.pref_site_login_two_factor_auth, scrollable = false)
}
onViewCreated(dialog.view)
return dialog
}
override fun setCredentialsOnView(view: View) = with(view) {
username.setText(service.getUsername())
password.setText(service.getPassword())
}
override fun checkLogin() {
v?.apply {
if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (two_factor_check.isChecked && two_factor_edit.text.isNullOrBlank())) {
errorResult()
context.toast(R.string.fields_cannot_be_blank)
return
}
login.progress = 1
dialog?.setCancelable(false)
dialog?.setCanceledOnTouchOutside(false)
scope.launch {
try {
val result = source?.login(
username.text.toString(),
password.text.toString(),
two_factor_edit.text.toString()
) ?: false
if (result) {
dialog?.dismiss()
preferences.setTrackCredentials(Injekt.get<TrackManager>().mdList, username.toString(), password.toString())
context.toast(R.string.login_success)
} else {
errorResult()
}
} catch (error: Exception) {
errorResult()
error.message?.let { context.toast(it) }
}
}
}
}
private fun errorResult() {
v?.apply {
dialog?.setCancelable(true)
dialog?.setCanceledOnTouchOutside(true)
login.progress = -1
login.setText(R.string.unknown_error)
}
}
override fun onDialogClosed() {
super.onDialogClosed()
if (activity != null) {
(activity as? Listener)?.siteLoginDialogClosed(source!!)
} else {
(targetController as? Listener)?.siteLoginDialogClosed(source!!)
}
}
interface Listener {
fun siteLoginDialogClosed(source: Source)
}
}
@@ -0,0 +1,49 @@
package exh.widget.preference
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
class MangadexLogoutDialog(bundle: Bundle? = null) : DialogController(bundle) {
val source by lazy { MdUtil.getEnabledMangaDex() }
val trackManager: TrackManager by injectLazy()
constructor(source: Source) : this(Bundle().apply { putLong("key", source.id) })
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.logout)
.positiveButton(R.string.logout) {
launchNow {
source?.let { source ->
val loggedOut = withContext(Dispatchers.IO) { source.logout() }
if (loggedOut) {
trackManager.mdList.logout()
activity?.toast(R.string.logout_success)
(targetController as? Listener)?.siteLogoutDialogClosed(source)
} else {
activity?.toast(R.string.unknown_error)
}
} ?: activity!!.toast("Mangadex not enabled")
}
}
.negativeButton(android.R.string.cancel)
}
interface Listener {
fun siteLogoutDialogClosed(source: Source)
}
}