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:
@@ -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,
|
||||
|
||||
@@ -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ñol]:",
|
||||
"[b] Spanish: [/ b]",
|
||||
"정보",
|
||||
"Spanish/Español",
|
||||
"Españ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ürkçe",
|
||||
"Turkish/Türkçe",
|
||||
"Türkç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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user