Finish some more advanced mangadex delegation features, more to come later
This commit is contained in:
parent
294ade035e
commit
0deb6f6b8d
@ -7,6 +7,7 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||||
// Realm (EH)
|
// Realm (EH)
|
||||||
apply plugin: 'realm-android'
|
apply plugin: 'realm-android'
|
||||||
@ -292,6 +293,10 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
||||||
|
|
||||||
|
// SY for mangadex utils
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0-RC"
|
||||||
|
|
||||||
|
|
||||||
final coroutines_version = '1.3.9'
|
final coroutines_version = '1.3.9'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
|
@ -282,7 +282,7 @@
|
|||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
|
|
||||||
<!-- MangaDex -->
|
<!-- MangaDex -->
|
||||||
<!-- <data
|
<data
|
||||||
android:host="mangadex.org"
|
android:host="mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="http" />
|
android:scheme="http" />
|
||||||
@ -297,7 +297,7 @@
|
|||||||
<data
|
<data
|
||||||
android:host="www.mangadex.org"
|
android:host="www.mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="https" />-->
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -286,4 +286,6 @@ object PreferenceKeys {
|
|||||||
const val groupLibraryUpdateType = "group_library_update_type"
|
const val groupLibraryUpdateType = "group_library_update_type"
|
||||||
|
|
||||||
const val useNewSourceNavigation = "use_new_source_navigation"
|
const val useNewSourceNavigation = "use_new_source_navigation"
|
||||||
|
|
||||||
|
const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers"
|
||||||
}
|
}
|
||||||
|
@ -391,4 +391,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
||||||
|
|
||||||
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, false)
|
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, false)
|
||||||
|
|
||||||
|
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,10 @@ class TrackManager(context: Context) {
|
|||||||
const val SHIKIMORI = 4
|
const val SHIKIMORI = 4
|
||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
|
|
||||||
|
// SY --> Mangadex from Neko todo
|
||||||
|
const val MDLIST = 60
|
||||||
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val REREADING = 2
|
const val REREADING = 2
|
||||||
|
@ -75,6 +75,11 @@ interface SManga : Serializable {
|
|||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val LICENSED = 3
|
const val LICENSED = 3
|
||||||
|
// SY --> Mangadex specific statuses
|
||||||
|
const val PUBLICATION_COMPLETE = 61
|
||||||
|
const val CANCELLED = 62
|
||||||
|
const val HIATUS = 63
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun create(): SManga {
|
fun create(): SManga {
|
||||||
return SMangaImpl()
|
return SMangaImpl()
|
||||||
|
@ -2,19 +2,44 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import exh.md.handlers.ApiChapterParser
|
||||||
|
import exh.md.handlers.ApiMangaParser
|
||||||
|
import exh.md.handlers.MangaHandler
|
||||||
|
import exh.md.handlers.MangaPlusHandler
|
||||||
|
import exh.md.utils.MdLang
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
|
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
|
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val lang: String = delegate.lang
|
override val lang: String = delegate.lang
|
||||||
|
|
||||||
|
private val mdLang by lazy {
|
||||||
|
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
||||||
|
}
|
||||||
|
|
||||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
@ -31,4 +56,46 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return MangaHandler(client, headers, listOf(mdLang)).fetchMangaDetailsObservable(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return MangaHandler(client, headers, listOf(mdLang)).fetchChapterListObservable(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
return if (chapter.scanlator == "MangaPlus") {
|
||||||
|
client.newCall(mangaPlusPageListRequest(chapter))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val chapterId = ApiChapterParser().externalParse(response)
|
||||||
|
MangaPlusHandler(client).fetchPageList(chapterId)
|
||||||
|
}
|
||||||
|
} else super.fetchPageList(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
|
||||||
|
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
|
||||||
|
return GET(MdUtil.baseUrl + chpUrl + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchImage(page: Page): Observable<Response> {
|
||||||
|
return if (page.imageUrl!!.contains("mangaplus", true)) {
|
||||||
|
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
} else super.fetchImage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): MangaDexDescriptionAdapter {
|
||||||
|
return MangaDexDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||||
|
ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
@ -118,7 +119,7 @@ class MangaPresenter(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (manga.initialized && source.isMetadataSource()) {
|
if (manga.initialized && source.isMetadataSource()) {
|
||||||
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else Timber.d("Invalid metadata") })
|
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@ -236,7 +237,7 @@ class MangaPresenter(
|
|||||||
// SY -->
|
// SY -->
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
if (source is MetadataSource<*, *> || (source is EnhancedHttpSource && source.enhancedSource is MetadataSource<*, *>)) {
|
if (source is MetadataSource<*, *> || (source is EnhancedHttpSource && source.enhancedSource is MetadataSource<*, *>)) {
|
||||||
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else Timber.d("Invalid metadata") })
|
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
@ -275,6 +275,11 @@ class MangaInfoHeaderAdapter(
|
|||||||
SManga.ONGOING -> R.string.ongoing
|
SManga.ONGOING -> R.string.ongoing
|
||||||
SManga.COMPLETED -> R.string.completed
|
SManga.COMPLETED -> R.string.completed
|
||||||
SManga.LICENSED -> R.string.licensed
|
SManga.LICENSED -> R.string.licensed
|
||||||
|
// SY --> Mangadex specific statuses
|
||||||
|
SManga.HIATUS -> R.string.hiatus
|
||||||
|
SManga.PUBLICATION_COMPLETE -> R.string.publication_complete
|
||||||
|
SManga.CANCELLED -> R.string.cancelled
|
||||||
|
// SY <--
|
||||||
else -> R.string.unknown_status
|
else -> R.string.unknown_status
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
34
app/src/main/java/exh/md/handlers/ApiChapterParser.kt
Normal file
34
app/src/main/java/exh/md/handlers/ApiChapterParser.kt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import java.util.Date
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class ApiChapterParser {
|
||||||
|
fun pageListParse(response: Response): List<Page> {
|
||||||
|
val jsonData = response.body!!.string()
|
||||||
|
val json = JsonParser.parseString(jsonData).asJsonObject
|
||||||
|
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
val hash = json.get("hash").string
|
||||||
|
val pageArray = json.getAsJsonArray("page_array")
|
||||||
|
val server = json.get("server").string
|
||||||
|
|
||||||
|
pageArray.forEach {
|
||||||
|
val url = "$hash/${it.asString}"
|
||||||
|
pages.add(Page(pages.size, "$server,${response.request.url},${Date().time}", url))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
fun externalParse(response: Response): String {
|
||||||
|
val jsonData = response.body!!.string()
|
||||||
|
val json = JsonParser.parseString(jsonData).asJsonObject
|
||||||
|
val external = json.get("external").string
|
||||||
|
return external.substringAfterLast("/")
|
||||||
|
}
|
||||||
|
}
|
301
app/src/main/java/exh/md/handlers/ApiMangaParser.kt
Normal file
301
app/src/main/java/exh/md/handlers/ApiMangaParser.kt
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import com.github.salomonbrys.kotson.nullInt
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import exh.md.handlers.serializers.ApiMangaSerializer
|
||||||
|
import exh.md.handlers.serializers.ChapterSerializer
|
||||||
|
import exh.md.utils.MdLang
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||||
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.math.floor
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Single
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class ApiMangaParser(val langs: List<String>) {
|
||||||
|
val db: DatabaseHelper get() = Injekt.get()
|
||||||
|
|
||||||
|
val metaClass = MangaDexSearchMetadata::class
|
||||||
|
/**
|
||||||
|
* Use reflection to create a new instance of metadata
|
||||||
|
*/
|
||||||
|
private fun newMetaInstance() = metaClass.constructors.find {
|
||||||
|
it.parameters.isEmpty()
|
||||||
|
}?.call()
|
||||||
|
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||||
|
/**
|
||||||
|
* Parses metadata from the input and then copies it into the manga
|
||||||
|
*
|
||||||
|
* Will also save the metadata to the DB if possible
|
||||||
|
*/
|
||||||
|
fun parseToManga(manga: SManga, input: Response): 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
|
||||||
|
Single.fromCallable {
|
||||||
|
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
||||||
|
}.map {
|
||||||
|
it?.raise(metaClass) ?: newMetaInstance()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Single.just(newMetaInstance())
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaObservable.map {
|
||||||
|
parseIntoMetadata(it, input)
|
||||||
|
it.copyTo(manga)
|
||||||
|
it
|
||||||
|
}.flatMapCompletable {
|
||||||
|
if (mangaId != null) {
|
||||||
|
it.mangaId = mangaId
|
||||||
|
db.insertFlatMetadata(it.flatten())
|
||||||
|
} else Completable.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||||
|
with(metadata) {
|
||||||
|
try {
|
||||||
|
val networkApiManga = MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), input.body!!.string())
|
||||||
|
val networkManga = networkApiManga.manga
|
||||||
|
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)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
networkManga.rating?.let {
|
||||||
|
rating = it.bayesian ?: it.mean
|
||||||
|
users = it.users
|
||||||
|
}
|
||||||
|
networkManga.links?.let { links ->
|
||||||
|
links.al?.let { anilist_id = it }
|
||||||
|
links.kt?.let { kitsu_id = it }
|
||||||
|
links.mal?.let { my_anime_list_id = it }
|
||||||
|
links.mu?.let { manga_updates_id = it }
|
||||||
|
links.ap?.let { anime_planet_id = it }
|
||||||
|
}
|
||||||
|
val filteredChapters = filterChapterForChecking(networkApiManga)
|
||||||
|
|
||||||
|
val tempStatus = parseStatus(networkManga.status)
|
||||||
|
val publishedOrCancelled =
|
||||||
|
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
|
||||||
|
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
missing_chapters = null
|
||||||
|
} else {
|
||||||
|
status = tempStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
val genres =
|
||||||
|
networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
if (networkManga.hentai == 1) {
|
||||||
|
genres.add("Hentai")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags.size != 0) tags.clear()
|
||||||
|
tags += genres.map { RaisedTag(null, it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If chapter title is oneshot or a chapter exists which matches the last chapter in the required language
|
||||||
|
* return manga is complete
|
||||||
|
*/
|
||||||
|
private fun isMangaCompleted(
|
||||||
|
serializer: ApiMangaSerializer,
|
||||||
|
filteredChapters: List<Map.Entry<String, ChapterSerializer>>
|
||||||
|
): Boolean {
|
||||||
|
if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val finalChapterNumber = serializer.manga.last_chapter!!
|
||||||
|
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
|
||||||
|
filteredChapters.firstOrNull()?.let {
|
||||||
|
if (isOneShot(it.value, finalChapterNumber)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val removeOneshots = filteredChapters.filter { !it.value.chapter.isNullOrBlank() }
|
||||||
|
return removeOneshots.size.toString() == floor(finalChapterNumber.toDouble()).toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<Map.Entry<String, ChapterSerializer>> {
|
||||||
|
serializer.chapter ?: return emptyList()
|
||||||
|
return serializer.chapter.entries
|
||||||
|
.filter { langs.contains(it.value.lang_code) }
|
||||||
|
.filter {
|
||||||
|
it.value.chapter?.let { chapterNumber ->
|
||||||
|
if (chapterNumber.toIntOrNull() == null) {
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
|
return@filter true
|
||||||
|
}
|
||||||
|
return@filter false
|
||||||
|
}.distinctBy { it.value.chapter }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean {
|
||||||
|
return chapter.title.equals("oneshot", true) ||
|
||||||
|
((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: Int) = when (status) {
|
||||||
|
1 -> SManga.ONGOING
|
||||||
|
2 -> SManga.PUBLICATION_COMPLETE
|
||||||
|
3 -> SManga.CANCELLED
|
||||||
|
4 -> SManga.HIATUS
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse for the random manga id from the [MdUtil.randMangaPage] response.
|
||||||
|
*/
|
||||||
|
fun randomMangaIdParse(response: Response): String {
|
||||||
|
val randMangaUrl = response.asJsoup()
|
||||||
|
.select("link[rel=canonical]")
|
||||||
|
.attr("href")
|
||||||
|
return MdUtil.getMangaId(randMangaUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
return chapterListParse(response.body!!.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chapterListParse(jsonData: String): List<SChapter> {
|
||||||
|
val now = Date().time
|
||||||
|
val networkApiManga =
|
||||||
|
MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), jsonData)
|
||||||
|
val networkManga = networkApiManga.manga
|
||||||
|
val networkChapters = networkApiManga.chapter
|
||||||
|
if (networkChapters.isNullOrEmpty()) {
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
val status = networkManga.status
|
||||||
|
|
||||||
|
val finalChapterNumber = networkManga.last_chapter!!
|
||||||
|
|
||||||
|
val chapters = mutableListOf<SChapter>()
|
||||||
|
|
||||||
|
// Skip chapters that don't match the desired language, or are future releases
|
||||||
|
|
||||||
|
val chapLangs = MdLang.values().filter { langs.contains(it.dexLang) }
|
||||||
|
networkChapters.filter { langs.contains(it.value.lang_code) && (it.value.timestamp * 1000) <= now }
|
||||||
|
.mapTo(chapters) { mapChapter(it.key, it.value, finalChapterNumber, status, chapLangs, networkChapters.size) }
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chapterParseForMangaId(response: Response): Int {
|
||||||
|
try {
|
||||||
|
if (response.code != 200) throw Exception("HTTP error ${response.code}")
|
||||||
|
val body = response.body?.string().orEmpty()
|
||||||
|
if (body.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonObject = JsonParser.parseString(body).obj
|
||||||
|
return jsonObject["manga_id"]?.nullInt ?: throw Exception("No manga associated with chapter")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapChapter(
|
||||||
|
chapterId: String,
|
||||||
|
networkChapter: ChapterSerializer,
|
||||||
|
finalChapterNumber: String,
|
||||||
|
status: Int,
|
||||||
|
chapLangs: List<MdLang>,
|
||||||
|
totalChapterCount: Int
|
||||||
|
): SChapter {
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.url = MdUtil.apiChapter + chapterId
|
||||||
|
val chapterName = mutableListOf<String>()
|
||||||
|
// Build chapter name
|
||||||
|
|
||||||
|
if (!networkChapter.volume.isNullOrBlank()) {
|
||||||
|
val vol = "Vol." + networkChapter.volume
|
||||||
|
chapterName.add(vol)
|
||||||
|
// todo
|
||||||
|
// chapter.vol = vol
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!networkChapter.chapter.isNullOrBlank()) {
|
||||||
|
val chp = "Ch." + networkChapter.chapter
|
||||||
|
chapterName.add(chp)
|
||||||
|
// chapter.chapter_txt = chp
|
||||||
|
}
|
||||||
|
if (!networkChapter.title.isNullOrBlank()) {
|
||||||
|
if (chapterName.isNotEmpty()) {
|
||||||
|
chapterName.add("-")
|
||||||
|
}
|
||||||
|
// todo
|
||||||
|
chapterName.add(networkChapter.title)
|
||||||
|
// chapter.chapter_title = MdUtil.cleanString(networkChapter.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if volume, chapter and title is empty its a oneshot
|
||||||
|
if (chapterName.isEmpty()) {
|
||||||
|
chapterName.add("Oneshot")
|
||||||
|
}
|
||||||
|
if ((status == 2 || status == 3)) {
|
||||||
|
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
|
||||||
|
networkChapter.chapter == finalChapterNumber
|
||||||
|
) {
|
||||||
|
chapterName.add("[END]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.name = MdUtil.cleanString(chapterName.joinToString(" "))
|
||||||
|
// Convert from unix time
|
||||||
|
chapter.date_upload = networkChapter.timestamp * 1000
|
||||||
|
val scanlatorName = mutableSetOf<String>()
|
||||||
|
|
||||||
|
networkChapter.group_name?.let {
|
||||||
|
scanlatorName.add(it)
|
||||||
|
}
|
||||||
|
networkChapter.group_name_2?.let {
|
||||||
|
scanlatorName.add(it)
|
||||||
|
}
|
||||||
|
networkChapter.group_name_3?.let {
|
||||||
|
scanlatorName.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName))
|
||||||
|
|
||||||
|
// chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url)
|
||||||
|
|
||||||
|
// chapter.language = chapLangs.firstOrNull { it.dexLang == networkChapter.lang_code }?.name
|
||||||
|
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/exh/md/handlers/CoverHandler.kt
Normal file
26
app/src/main/java/exh/md/handlers/CoverHandler.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
179
app/src/main/java/exh/md/handlers/FilterHandler.kt
Normal file
179
app/src/main/java/exh/md/handlers/FilterHandler.kt
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
class FilterHandler {
|
||||||
|
|
||||||
|
class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
class Tag(val id: String, name: String) : Filter.TriState(name)
|
||||||
|
class Switch(val id: String, name: String) : Filter.CheckBox(name)
|
||||||
|
class ContentList(contents: List<Tag>) : Filter.Group<Tag>("Content", contents)
|
||||||
|
class FormatList(formats: List<Tag>) : Filter.Group<Tag>("Format", formats)
|
||||||
|
class GenreList(genres: List<Tag>) : Filter.Group<Tag>("Genres", genres)
|
||||||
|
class PublicationStatusList(statuses: List<Switch>) : Filter.Group<Switch>("Publication Status", statuses)
|
||||||
|
class DemographicList(demographics: List<Switch>) : Filter.Group<Switch>("Demographic", demographics)
|
||||||
|
|
||||||
|
class R18 : Filter.Select<String>("R18+", arrayOf("Default", "Show all", "Show only", "Show none"))
|
||||||
|
class ThemeList(themes: List<Tag>) : Filter.Group<Tag>("Themes", themes)
|
||||||
|
class TagInclusionMode : Filter.Select<String>("Tag inclusion", arrayOf("All (and)", "Any (or)"), 0)
|
||||||
|
class TagExclusionMode : Filter.Select<String>("Tag exclusion", arrayOf("All (and)", "Any (or)"), 1)
|
||||||
|
|
||||||
|
class SortFilter : Filter.Sort(
|
||||||
|
"Sort",
|
||||||
|
sortables().map { it.first }.toTypedArray(),
|
||||||
|
Selection(0, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
class OriginalLanguage : Filter.Select<String>("Original Language", sourceLang().map { it.first }.toTypedArray())
|
||||||
|
|
||||||
|
fun getFilterList() = FilterList(
|
||||||
|
TextField("Author", "author"),
|
||||||
|
TextField("Artist", "artist"),
|
||||||
|
R18(),
|
||||||
|
SortFilter(),
|
||||||
|
DemographicList(demographics()),
|
||||||
|
PublicationStatusList(publicationStatus()),
|
||||||
|
OriginalLanguage(),
|
||||||
|
ContentList(contentType()),
|
||||||
|
FormatList(formats()),
|
||||||
|
GenreList(genre()),
|
||||||
|
ThemeList(themes()),
|
||||||
|
TagInclusionMode(),
|
||||||
|
TagExclusionMode()
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun demographics() = listOf(
|
||||||
|
Switch("1", "Shounen"),
|
||||||
|
Switch("2", "Shoujo"),
|
||||||
|
Switch("3", "Seinen"),
|
||||||
|
Switch("4", "Josei")
|
||||||
|
)
|
||||||
|
|
||||||
|
fun publicationStatus() = listOf(
|
||||||
|
Switch("1", "Ongoing"),
|
||||||
|
Switch("2", "Completed"),
|
||||||
|
Switch("3", "Cancelled"),
|
||||||
|
Switch("4", "Hiatus")
|
||||||
|
)
|
||||||
|
|
||||||
|
fun sortables() = listOf(
|
||||||
|
Triple("Update date", 1, 0),
|
||||||
|
Triple("Alphabetically", 2, 3),
|
||||||
|
Triple("Number of comments", 4, 5),
|
||||||
|
Triple("Rating", 6, 7),
|
||||||
|
Triple("Views", 8, 9),
|
||||||
|
Triple("Follows", 10, 11)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun sourceLang() = listOf(
|
||||||
|
Pair("All", "0"),
|
||||||
|
Pair("Japanese", "2"),
|
||||||
|
Pair("English", "1"),
|
||||||
|
Pair("Polish", "3"),
|
||||||
|
Pair("German", "8"),
|
||||||
|
Pair("French", "10"),
|
||||||
|
Pair("Vietnamese", "12"),
|
||||||
|
Pair("Chinese", "21"),
|
||||||
|
Pair("Indonesian", "27"),
|
||||||
|
Pair("Korean", "28"),
|
||||||
|
Pair("Spanish (LATAM)", "29"),
|
||||||
|
Pair("Thai", "32"),
|
||||||
|
Pair("Filipino", "34")
|
||||||
|
)
|
||||||
|
|
||||||
|
fun contentType() = listOf(
|
||||||
|
Tag("9", "Ecchi"),
|
||||||
|
Tag("32", "Smut"),
|
||||||
|
Tag("49", "Gore"),
|
||||||
|
Tag("50", "Sexual Violence")
|
||||||
|
).sortedWith(compareBy { it.name })
|
||||||
|
|
||||||
|
fun formats() = listOf(
|
||||||
|
Tag("1", "4-koma"),
|
||||||
|
Tag("4", "Award Winning"),
|
||||||
|
Tag("7", "Doujinshi"),
|
||||||
|
Tag("21", "Oneshot"),
|
||||||
|
Tag("36", "Long Strip"),
|
||||||
|
Tag("42", "Adaptation"),
|
||||||
|
Tag("43", "Anthology"),
|
||||||
|
Tag("44", "Web Comic"),
|
||||||
|
Tag("45", "Full Color"),
|
||||||
|
Tag("46", "User Created"),
|
||||||
|
Tag("47", "Official Colored"),
|
||||||
|
Tag("48", "Fan Colored")
|
||||||
|
).sortedWith(compareBy { it.name })
|
||||||
|
|
||||||
|
fun genre() = listOf(
|
||||||
|
Tag("2", "Action"),
|
||||||
|
Tag("3", "Adventure"),
|
||||||
|
Tag("5", "Comedy"),
|
||||||
|
Tag("8", "Drama"),
|
||||||
|
Tag("10", "Fantasy"),
|
||||||
|
Tag("13", "Historical"),
|
||||||
|
Tag("14", "Horror"),
|
||||||
|
Tag("17", "Mecha"),
|
||||||
|
Tag("18", "Medical"),
|
||||||
|
Tag("20", "Mystery"),
|
||||||
|
Tag("22", "Psychological"),
|
||||||
|
Tag("23", "Romance"),
|
||||||
|
Tag("25", "Sci-Fi"),
|
||||||
|
Tag("28", "Shoujo Ai"),
|
||||||
|
Tag("30", "Shounen Ai"),
|
||||||
|
Tag("31", "Slice of Life"),
|
||||||
|
Tag("33", "Sports"),
|
||||||
|
Tag("35", "Tragedy"),
|
||||||
|
Tag("37", "Yaoi"),
|
||||||
|
Tag("38", "Yuri"),
|
||||||
|
Tag("41", "Isekai"),
|
||||||
|
Tag("51", "Crime"),
|
||||||
|
Tag("52", "Magical Girls"),
|
||||||
|
Tag("53", "Philosophical"),
|
||||||
|
Tag("54", "Superhero"),
|
||||||
|
Tag("55", "Thriller"),
|
||||||
|
Tag("56", "Wuxia")
|
||||||
|
).sortedWith(compareBy { it.name })
|
||||||
|
|
||||||
|
fun themes() = listOf(
|
||||||
|
Tag("6", "Cooking"),
|
||||||
|
Tag("11", "Gyaru"),
|
||||||
|
Tag("12", "Harem"),
|
||||||
|
Tag("16", "Martial Arts"),
|
||||||
|
Tag("19", "Music"),
|
||||||
|
Tag("24", "School Life"),
|
||||||
|
Tag("34", "Supernatural"),
|
||||||
|
Tag("40", "Video Games"),
|
||||||
|
Tag("57", "Aliens"),
|
||||||
|
Tag("58", "Animals"),
|
||||||
|
Tag("59", "Crossdressing"),
|
||||||
|
Tag("60", "Demons"),
|
||||||
|
Tag("61", "Delinquents"),
|
||||||
|
Tag("62", "Genderswap"),
|
||||||
|
Tag("63", "Ghosts"),
|
||||||
|
Tag("64", "Monster Girls"),
|
||||||
|
Tag("65", "Loli"),
|
||||||
|
Tag("66", "Magic"),
|
||||||
|
Tag("67", "Military"),
|
||||||
|
Tag("68", "Monsters"),
|
||||||
|
Tag("69", "Ninja"),
|
||||||
|
Tag("70", "Office Workers"),
|
||||||
|
Tag("71", "Police"),
|
||||||
|
Tag("72", "Post-Apocalyptic"),
|
||||||
|
Tag("73", "Reincarnation"),
|
||||||
|
Tag("74", "Reverse Harem"),
|
||||||
|
Tag("75", "Samurai"),
|
||||||
|
Tag("76", "Shota"),
|
||||||
|
Tag("77", "Survival"),
|
||||||
|
Tag("78", "Time Travel"),
|
||||||
|
Tag("79", "Vampires"),
|
||||||
|
Tag("80", "Traditional Games"),
|
||||||
|
Tag("81", "Virtual Reality"),
|
||||||
|
Tag("82", "Zombies"),
|
||||||
|
Tag("83", "Incest"),
|
||||||
|
Tag("84", "Mafia")
|
||||||
|
).sortedWith(compareBy { it.name })
|
||||||
|
|
||||||
|
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap()
|
||||||
|
}
|
||||||
|
}
|
240
app/src/main/java/exh/md/handlers/FollowsHandler.kt
Normal file
240
app/src/main/java/exh/md/handlers/FollowsHandler.kt
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
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.MetadataMangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.md.handlers.serializers.FollowsPageResult
|
||||||
|
import exh.md.handlers.serializers.Result
|
||||||
|
import exh.md.utils.FollowStatus
|
||||||
|
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 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))
|
||||||
|
.asObservable()
|
||||||
|
.map { response ->
|
||||||
|
followsParseMangaPage(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse follows api to manga page
|
||||||
|
* used when multiple follows
|
||||||
|
*/
|
||||||
|
private fun followsParseMangaPage(response: Response, forceHd: Boolean = false): MetadataMangasPage {
|
||||||
|
var followsPageResult: FollowsPageResult? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
followsPageResult =
|
||||||
|
MdUtil.jsonParser.decodeFromString(
|
||||||
|
FollowsPageResult.serializer(),
|
||||||
|
response.body!!.string()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.e("error parsing follows", e)
|
||||||
|
}
|
||||||
|
val empty = followsPageResult?.result?.isEmpty()
|
||||||
|
|
||||||
|
if (empty == null || empty) {
|
||||||
|
return MetadataMangasPage(mutableListOf(), false, mutableListOf())
|
||||||
|
}
|
||||||
|
val lowQualityCovers = if (forceHd) false else preferences.mangaDexLowQualityCovers().get()
|
||||||
|
|
||||||
|
val follows = followsPageResult!!.result.map {
|
||||||
|
followFromElement(it, lowQualityCovers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetadataMangasPage(follows.map { it.first }, true, follows.map { it.second })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**fetch follow status used when fetching status for 1 manga
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun followStatusParse(response: Response): Track {
|
||||||
|
var followsPageResult: FollowsPageResult? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
followsPageResult =
|
||||||
|
MdUtil.jsonParser.decodeFromString(
|
||||||
|
FollowsPageResult.serializer(),
|
||||||
|
response.body!!.string()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.e("error parsing follows", e)
|
||||||
|
}
|
||||||
|
val track = Track.create(TrackManager.MDLIST)
|
||||||
|
val result = followsPageResult?.result
|
||||||
|
if (result.isNullOrEmpty()) {
|
||||||
|
track.status = FollowStatus.UNFOLLOWED.int
|
||||||
|
} else {
|
||||||
|
val follow = result.first()
|
||||||
|
track.status = follow.follow_type
|
||||||
|
if (result[0].chapter.isNotBlank()) {
|
||||||
|
track.last_chapter_read = floor(follow.chapter.toFloat()).toInt()
|
||||||
|
}
|
||||||
|
track.tracking_url = MdUtil.baseUrl + follow.manga_id.toString()
|
||||||
|
track.title = follow.title
|
||||||
|
}
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
/**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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse result element to manga
|
||||||
|
*/
|
||||||
|
private fun followFromElement(result: Result, lowQualityCovers: Boolean): Pair<SManga, MangaDexSearchMetadata> {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.title = MdUtil.cleanString(result.title)
|
||||||
|
manga.url = "/manga/${result.manga_id}/"
|
||||||
|
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers)
|
||||||
|
return manga to MangaDexSearchMetadata().apply {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the status of a manga
|
||||||
|
*/
|
||||||
|
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response: Response =
|
||||||
|
if (followStatus == FollowStatus.UNFOLLOWED) {
|
||||||
|
client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/ajax/actions.ajax.php?function=manga_unfollow&id=$mangaID&type=$mangaID",
|
||||||
|
headers,
|
||||||
|
CacheControl.FORCE_NETWORK
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
} else {
|
||||||
|
val status = followStatus.int
|
||||||
|
client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/ajax/actions.ajax.php?function=manga_follow&id=$mangaID&type=$status",
|
||||||
|
headers,
|
||||||
|
CacheControl.FORCE_NETWORK
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
response.body!!.string().isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateReadingProgress(track: Track): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val mangaID = getMangaId(track.tracking_url)
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("chapter", track.last_chapter_read.toString())
|
||||||
|
XLog.d("chapter to update %s", track.last_chapter_read.toString())
|
||||||
|
val response = client.newCall(
|
||||||
|
POST(
|
||||||
|
"$baseUrl/ajax/actions.ajax.php?function=edit_progress&id=$mangaID",
|
||||||
|
headers,
|
||||||
|
formBody.build()
|
||||||
|
)
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
val response2 = client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
response.body!!.string().isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateRating(track: Track): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val mangaID = getMangaId(track.tracking_url)
|
||||||
|
val response = client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
response.body!!.string().isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch all manga from all possible pages
|
||||||
|
*/
|
||||||
|
suspend fun fetchAllFollows(forceHd: Boolean): List<SManga> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if (!mangasPage.hasNextPage) {
|
||||||
|
break@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listManga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchTrackingInfo(url: String): Track {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val request = GET(
|
||||||
|
"${MdUtil.baseUrl}${MdUtil.followsMangaApi}" + getMangaId(url),
|
||||||
|
headers,
|
||||||
|
CacheControl.FORCE_NETWORK
|
||||||
|
)
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val track = followStatusParse(response)
|
||||||
|
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
app/src/main/java/exh/md/handlers/MangaHandler.kt
Normal file
102
app/src/main/java/exh/md/handlers/MangaHandler.kt
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List<String>) {
|
||||||
|
|
||||||
|
// TODO make use of this
|
||||||
|
suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair<SManga, List<SChapter>> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response = client.newCall(apiRequest(manga)).execute()
|
||||||
|
val parser = ApiMangaParser(langs)
|
||||||
|
|
||||||
|
val jsonData = response.body!!.string()
|
||||||
|
if (response.code != 200) {
|
||||||
|
XLog.e("error from MangaDex with response code ${response.code} \n body: \n$jsonData")
|
||||||
|
throw Exception("Error from MangaDex Response code ${response.code} ")
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.parseToManga(manga, response).await()
|
||||||
|
val chapterList = parser.chapterListParse(jsonData)
|
||||||
|
Pair(
|
||||||
|
manga,
|
||||||
|
chapterList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaIdFromChapterId(urlChapterId: String): Int {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val request = GET(MdUtil.baseUrl + MdUtil.apiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
ApiMangaParser(langs).chapterParseForMangaId(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchMangaDetails(manga: SManga): SManga {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response = client.newCall(apiRequest(manga)).execute()
|
||||||
|
ApiMangaParser(langs).parseToManga(manga, response).await()
|
||||||
|
manga.apply {
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchMangaDetailsObservable(manga: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(apiRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.flatMap {
|
||||||
|
ApiMangaParser(langs).parseToManga(manga, it).andThen(
|
||||||
|
Observable.just(
|
||||||
|
manga.apply {
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchChapterListObservable(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return client.newCall(apiRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
ApiMangaParser(langs).chapterListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchChapterList(manga: SManga): List<SChapter> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val response = client.newCall(apiRequest(manga)).execute()
|
||||||
|
ApiMangaParser(langs).chapterListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchRandomMangaId(): Observable<String> {
|
||||||
|
return client.newCall(randomMangaRequest())
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
ApiMangaParser(langs).randomMangaIdParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomMangaRequest(): Request {
|
||||||
|
return GET(MdUtil.baseUrl + MdUtil.randMangaPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apiRequest(manga: SManga): Request {
|
||||||
|
return GET(MdUtil.baseUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url), headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
}
|
105
app/src/main/java/exh/md/handlers/MangaPlusHandler.kt
Normal file
105
app/src/main/java/exh/md/handlers/MangaPlusHandler.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import MangaPlusSerializer
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
|
||||||
|
class MangaPlusHandler(currentClient: OkHttpClient) {
|
||||||
|
val baseUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
|
||||||
|
val headers = Headers.Builder()
|
||||||
|
.add("Origin", WEB_URL)
|
||||||
|
.add("Referer", WEB_URL)
|
||||||
|
.add("User-Agent", USER_AGENT)
|
||||||
|
.add("SESSION-TOKEN", UUID.randomUUID().toString()).build()
|
||||||
|
|
||||||
|
val client: OkHttpClient = currentClient.newBuilder()
|
||||||
|
.addInterceptor { imageIntercept(it) }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
fun fetchPageList(chapterId: String): List<Page> {
|
||||||
|
val response = client.newCall(pageListRequest(chapterId)).execute()
|
||||||
|
return pageListParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageListRequest(chapterId: String): Request {
|
||||||
|
return GET(
|
||||||
|
"$baseUrl/manga_viewer?chapter_id=$chapterId&split=yes&img_quality=high",
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
private fun pageListParse(response: Response): List<Page> {
|
||||||
|
val result = ProtoBuf.decodeFromByteArray(MangaPlusSerializer, response.body!!.bytes())
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception("error getting images")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.success.mangaViewer!!.pages
|
||||||
|
.mapNotNull { it.page }
|
||||||
|
.mapIndexed { i, page ->
|
||||||
|
val encryptionKey =
|
||||||
|
if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}"
|
||||||
|
Page(i, "", "${page.imageUrl}$encryptionKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun imageIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
var request = chain.request()
|
||||||
|
|
||||||
|
if (!request.url.queryParameterNames.contains("encryptionKey")) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val encryptionKey = request.url.queryParameter("encryptionKey")!!
|
||||||
|
|
||||||
|
// Change the url and remove the encryptionKey to avoid detection.
|
||||||
|
val newUrl = request.url.newBuilder().removeAllQueryParameters("encryptionKey").build()
|
||||||
|
request = request.newBuilder().url(newUrl).build()
|
||||||
|
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
|
val image = decodeImage(encryptionKey, response.body!!.bytes())
|
||||||
|
|
||||||
|
val body = image.toResponseBody("image/jpeg".toMediaTypeOrNull())
|
||||||
|
return response.newBuilder().body(body).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray {
|
||||||
|
val keyStream = HEX_GROUP
|
||||||
|
.findAll(encryptionKey)
|
||||||
|
.map { it.groupValues[1].toInt(16) }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val content = image
|
||||||
|
.map { it.toInt() }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
val blockSizeInBytes = keyStream.size
|
||||||
|
|
||||||
|
for ((i, value) in content.iterator().withIndex()) {
|
||||||
|
content[i] = value xor keyStream[i % blockSizeInBytes]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ByteArray(content.size) { pos -> content[pos].toByte() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val WEB_URL = "https://mangaplus.shueisha.co.jp"
|
||||||
|
private const val USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36"
|
||||||
|
private val HEX_GROUP = "(.{1,2})".toRegex()
|
||||||
|
}
|
||||||
|
}
|
37
app/src/main/java/exh/md/handlers/PageHandler.kt
Normal file
37
app/src/main/java/exh/md/handlers/PageHandler.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
// Unused, kept for reference todo
|
||||||
|
class PageHandler(val client: OkHttpClient, val headers: Headers, private val imageServer: String, val dataSaver: String?) {
|
||||||
|
|
||||||
|
fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
if (chapter.scanlator.equals("MangaPlus")) {
|
||||||
|
return client.newCall(pageListRequest(chapter))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val chapterId = ApiChapterParser().externalParse(response)
|
||||||
|
MangaPlusHandler(client).fetchPageList(chapterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client.newCall(pageListRequest(chapter))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
ApiChapterParser().pageListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
|
||||||
|
return GET("${MdUtil.baseUrl}${chpUrl}${MdUtil.apiChapterSuffix}&server=$imageServer&saver=$dataSaver", headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
}
|
71
app/src/main/java/exh/md/handlers/PopularHandler.kt
Normal file
71
app/src/main/java/exh/md/handlers/PopularHandler.kt
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.md.utils.setMDUrlWithoutDomain
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
// Unused, kept for reference todo
|
||||||
|
/**
|
||||||
|
* Returns the latest manga from the updates url since it actually respects the users settings
|
||||||
|
*/
|
||||||
|
class PopularHandler(val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
return client.newCall(popularMangaRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
popularMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("${MdUtil.baseUrl}/updates/$page/", headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(popularMangaSelector).map { element ->
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
}.distinct()
|
||||||
|
|
||||||
|
val hasNextPage = popularMangaNextPageSelector.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.manga_title").first().let {
|
||||||
|
val url = MdUtil.modifyMangaUrl(it.attr("href"))
|
||||||
|
manga.setMDUrlWithoutDomain(url)
|
||||||
|
manga.title = it.text().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, preferences.mangaDexLowQualityCovers().get())
|
||||||
|
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val popularMangaSelector = "tr a.manga_title"
|
||||||
|
const val popularMangaNextPageSelector = ".pagination li:not(.disabled) span[title*=last page]:not(disabled)"
|
||||||
|
}
|
||||||
|
}
|
216
app/src/main/java/exh/md/handlers/SearchHandler.kt
Normal file
216
app/src/main/java/exh/md/handlers/SearchHandler.kt
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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.util.asJsoup
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.md.utils.setMDUrlWithoutDomain
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
// Unused, kept for reference todo
|
||||||
|
class SearchHandler(val client: OkHttpClient, private val headers: Headers, val langs: List<String>) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return when {
|
||||||
|
query.startsWith(PREFIX_ID_SEARCH) -> {
|
||||||
|
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
|
||||||
|
client.newCall(searchMangaByIdRequest(realQuery))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val details = SManga.create()
|
||||||
|
details.url = "/manga/$realQuery/"
|
||||||
|
ApiMangaParser(langs).parseToManga(details, response).await()
|
||||||
|
MangasPage(listOf(details), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query.startsWith(PREFIX_GROUP_SEARCH) -> {
|
||||||
|
val realQuery = query.removePrefix(PREFIX_GROUP_SEARCH)
|
||||||
|
client.newCall(searchMangaByGroupRequest(realQuery))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
response.asJsoup().select(groupSelector).firstOrNull()?.attr("abs:href")
|
||||||
|
?.let {
|
||||||
|
searchMangaParse(client.newCall(GET("$it/manga/0", headers)).execute())
|
||||||
|
}
|
||||||
|
?: MangasPage(emptyList(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
searchMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(searchMangaSelector).map { element ->
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = searchMangaNextPageSelector.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val tags = mutableListOf<String>()
|
||||||
|
val statuses = mutableListOf<String>()
|
||||||
|
val demographics = mutableListOf<String>()
|
||||||
|
|
||||||
|
// Do traditional search
|
||||||
|
val url = "${MdUtil.baseUrl}/?page=search".toHttpUrlOrNull()!!.newBuilder()
|
||||||
|
.addQueryParameter("p", page.toString())
|
||||||
|
.addQueryParameter("title", query.replace(WHITESPACE_REGEX, " "))
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is FilterHandler.TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||||
|
is FilterHandler.DemographicList -> {
|
||||||
|
filter.state.forEach { demographic ->
|
||||||
|
if (demographic.state) {
|
||||||
|
demographics.add(demographic.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.PublicationStatusList -> {
|
||||||
|
filter.state.forEach { status ->
|
||||||
|
if (status.state) {
|
||||||
|
statuses.add(status.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.OriginalLanguage -> {
|
||||||
|
if (filter.state != 0) {
|
||||||
|
val number: String =
|
||||||
|
FilterHandler.sourceLang().first { it -> it.first == filter.values[filter.state] }
|
||||||
|
.second
|
||||||
|
url.addQueryParameter("lang_id", number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.TagInclusionMode -> {
|
||||||
|
url.addQueryParameter("tag_mode_inc", arrayOf("all", "any")[filter.state])
|
||||||
|
}
|
||||||
|
is FilterHandler.TagExclusionMode -> {
|
||||||
|
url.addQueryParameter("tag_mode_exc", arrayOf("all", "any")[filter.state])
|
||||||
|
}
|
||||||
|
is FilterHandler.ContentList -> {
|
||||||
|
filter.state.forEach { content ->
|
||||||
|
if (content.isExcluded()) {
|
||||||
|
tags.add("-${content.id}")
|
||||||
|
} else if (content.isIncluded()) {
|
||||||
|
tags.add(content.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.FormatList -> {
|
||||||
|
filter.state.forEach { format ->
|
||||||
|
if (format.isExcluded()) {
|
||||||
|
tags.add("-${format.id}")
|
||||||
|
} else if (format.isIncluded()) {
|
||||||
|
tags.add(format.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.GenreList -> {
|
||||||
|
filter.state.forEach { genre ->
|
||||||
|
if (genre.isExcluded()) {
|
||||||
|
tags.add("-${genre.id}")
|
||||||
|
} else if (genre.isIncluded()) {
|
||||||
|
tags.add(genre.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.ThemeList -> {
|
||||||
|
filter.state.forEach { theme ->
|
||||||
|
if (theme.isExcluded()) {
|
||||||
|
tags.add("-${theme.id}")
|
||||||
|
} else if (theme.isIncluded()) {
|
||||||
|
tags.add(theme.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FilterHandler.SortFilter -> {
|
||||||
|
if (filter.state != null) {
|
||||||
|
val sortables = FilterHandler.sortables()
|
||||||
|
if (filter.state!!.ascending) {
|
||||||
|
url.addQueryParameter(
|
||||||
|
"s",
|
||||||
|
sortables[filter.state!!.index].second.toString()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
url.addQueryParameter(
|
||||||
|
"s",
|
||||||
|
sortables[filter.state!!.index].third.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Manually append genres list to avoid commas being encoded
|
||||||
|
var urlToUse = url.toString()
|
||||||
|
if (demographics.isNotEmpty()) {
|
||||||
|
urlToUse += "&demos=" + demographics.joinToString(",")
|
||||||
|
}
|
||||||
|
if (statuses.isNotEmpty()) {
|
||||||
|
urlToUse += "&statuses=" + statuses.joinToString(",")
|
||||||
|
}
|
||||||
|
if (tags.isNotEmpty()) {
|
||||||
|
urlToUse += "&tags=" + tags.joinToString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(urlToUse, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.manga_title").first().let {
|
||||||
|
val url = MdUtil.modifyMangaUrl(it.attr("href"))
|
||||||
|
manga.setMDUrlWithoutDomain(url)
|
||||||
|
manga.title = it.text().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, preferences.mangaDexLowQualityCovers().get())
|
||||||
|
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaByIdRequest(id: String): Request {
|
||||||
|
return GET(MdUtil.baseUrl + MdUtil.apiManga + id, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaByGroupRequest(group: String): Request {
|
||||||
|
return GET(MdUtil.groupSearchUrl + group, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PREFIX_ID_SEARCH = "id:"
|
||||||
|
const val PREFIX_GROUP_SEARCH = "group:"
|
||||||
|
val WHITESPACE_REGEX = "\\s".toRegex()
|
||||||
|
const val searchMangaNextPageSelector =
|
||||||
|
".pagination li:not(.disabled) span[title*=last page]:not(disabled)"
|
||||||
|
const val searchMangaSelector = "div.manga-entry"
|
||||||
|
const val groupSelector = ".table > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(2) > a"
|
||||||
|
}
|
||||||
|
}
|
47
app/src/main/java/exh/md/handlers/SimilarHandler.kt
Normal file
47
app/src/main/java/exh/md/handlers/SimilarHandler.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
// todo make this work
|
||||||
|
/*import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSimilarImpl
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class SimilarHandler(val preferences: PreferencesHelper) {
|
||||||
|
|
||||||
|
*//**
|
||||||
|
* fetch our similar mangas
|
||||||
|
*//*
|
||||||
|
fun fetchSimilar(manga: Manga): Observable<MangasPage> {
|
||||||
|
|
||||||
|
// Parse the Mangadex id from the URL
|
||||||
|
val mangaid = MdUtil.getMangaId(manga.url).toLong()
|
||||||
|
|
||||||
|
val lowQualityCovers = preferences.mangaDexLowQualityCovers().get()
|
||||||
|
|
||||||
|
// Get our current database
|
||||||
|
val db = Injekt.get<DatabaseHelper>()
|
||||||
|
val similarMangaDb = db.getSimilar(mangaid).executeAsBlocking() ?: return Observable.just(MangasPage(mutableListOf(), false))
|
||||||
|
|
||||||
|
// Check if we have a result
|
||||||
|
|
||||||
|
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
|
||||||
|
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
|
||||||
|
|
||||||
|
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
|
||||||
|
SManga.create().apply {
|
||||||
|
title = similarMangaTitles[index]
|
||||||
|
url = "/manga/$similarId/"
|
||||||
|
thumbnail_url = MdUtil.formThumbUrl(url, lowQualityCovers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the matches
|
||||||
|
return Observable.just(MangasPage(similarMangas, false))
|
||||||
|
}
|
||||||
|
}*/
|
@ -0,0 +1,74 @@
|
|||||||
|
package exh.md.handlers.serializers
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiMangaSerializer(
|
||||||
|
val chapter: Map<String, ChapterSerializer>? = null,
|
||||||
|
val manga: MangaSerializer,
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaSerializer(
|
||||||
|
val artist: String,
|
||||||
|
val author: String,
|
||||||
|
val cover_url: String,
|
||||||
|
val description: String,
|
||||||
|
val genres: List<Int>,
|
||||||
|
val hentai: Int,
|
||||||
|
val lang_flag: String,
|
||||||
|
val lang_name: String,
|
||||||
|
val last_chapter: String? = null,
|
||||||
|
val links: LinksSerializer? = null,
|
||||||
|
val rating: RatingSerializer? = null,
|
||||||
|
val status: Int,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LinksSerializer(
|
||||||
|
val al: String? = null,
|
||||||
|
val amz: String? = null,
|
||||||
|
val ap: String? = null,
|
||||||
|
val engtl: String? = null,
|
||||||
|
val kt: String? = null,
|
||||||
|
val mal: String? = null,
|
||||||
|
val mu: String? = null,
|
||||||
|
val raw: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RatingSerializer(
|
||||||
|
val bayesian: String? = null,
|
||||||
|
val mean: String? = null,
|
||||||
|
val users: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChapterSerializer(
|
||||||
|
val volume: String? = null,
|
||||||
|
val chapter: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val lang_code: String,
|
||||||
|
val group_id: Int? = null,
|
||||||
|
val group_name: String? = null,
|
||||||
|
val group_id_2: Int? = null,
|
||||||
|
val group_name_2: String? = null,
|
||||||
|
val group_id_3: Int? = null,
|
||||||
|
val group_name_3: String? = null,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CoversResult(
|
||||||
|
val covers: List<String> = emptyList(),
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ImageReportResult(
|
||||||
|
val url: String,
|
||||||
|
val success: Boolean,
|
||||||
|
val bytes: Int?
|
||||||
|
)
|
@ -0,0 +1,17 @@
|
|||||||
|
package exh.md.handlers.serializers
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FollowsPageResult(
|
||||||
|
val result: List<Result> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Result(
|
||||||
|
val title: String,
|
||||||
|
val chapter: String,
|
||||||
|
val follow_type: Int,
|
||||||
|
val manga_id: Int,
|
||||||
|
val volume: String
|
||||||
|
)
|
@ -0,0 +1,136 @@
|
|||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializer(forClass = MangaPlusResponse::class)
|
||||||
|
object MangaPlusSerializer
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class MangaPlusResponse(
|
||||||
|
@ProtoNumber(1) val success: SuccessResult? = null,
|
||||||
|
@ProtoNumber(2) val error: ErrorResult? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class ErrorResult(
|
||||||
|
@ProtoNumber(1) val action: Action,
|
||||||
|
@ProtoNumber(2) val englishPopup: Popup,
|
||||||
|
@ProtoNumber(3) val spanishPopup: Popup
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class Action { DEFAULT, UNAUTHORIZED, MAINTAINENCE, GEOIP_BLOCKING }
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class Popup(
|
||||||
|
@ProtoNumber(1) val subject: String,
|
||||||
|
@ProtoNumber(2) val body: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class SuccessResult(
|
||||||
|
@ProtoNumber(1) val isFeaturedUpdated: Boolean? = false,
|
||||||
|
@ProtoNumber(5) val allTitlesView: AllTitlesView? = null,
|
||||||
|
@ProtoNumber(6) val titleRankingView: TitleRankingView? = null,
|
||||||
|
@ProtoNumber(8) val titleDetailView: TitleDetailView? = null,
|
||||||
|
@ProtoNumber(10) val mangaViewer: MangaViewer? = null,
|
||||||
|
@ProtoNumber(11) val webHomeView: WebHomeView? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class TitleRankingView(@ProtoNumber(1) val titles: List<Title> = emptyList())
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class AllTitlesView(@ProtoNumber(1) val titles: List<Title> = emptyList())
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class WebHomeView(@ProtoNumber(2) val groups: List<UpdatedTitleGroup> = emptyList())
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class TitleDetailView(
|
||||||
|
@ProtoNumber(1) val title: Title,
|
||||||
|
@ProtoNumber(2) val titleImageUrl: String,
|
||||||
|
@ProtoNumber(3) val overview: String,
|
||||||
|
@ProtoNumber(4) val backgroundImageUrl: String,
|
||||||
|
@ProtoNumber(5) val nextTimeStamp: Int = 0,
|
||||||
|
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
|
||||||
|
@ProtoNumber(7) val viewingPeriodDescription: String = "",
|
||||||
|
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
|
||||||
|
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
|
||||||
|
@ProtoNumber(14) val isSimulReleased: Boolean = true,
|
||||||
|
@ProtoNumber(17) val chaptersDescending: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class UpdateTiming { NOT_REGULARLY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, DAY }
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList())
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class Title(
|
||||||
|
@ProtoNumber(1) val titleId: Int,
|
||||||
|
@ProtoNumber(2) val name: String,
|
||||||
|
@ProtoNumber(3) val author: String,
|
||||||
|
@ProtoNumber(4) val portraitImageUrl: String,
|
||||||
|
@ProtoNumber(5) val landscapeImageUrl: String,
|
||||||
|
@ProtoNumber(6) val viewCount: Int,
|
||||||
|
@ProtoNumber(7) val language: Language? = Language.ENGLISH
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
enum class Language(val id: Int) {
|
||||||
|
@ProtoNumber(0)
|
||||||
|
ENGLISH(0),
|
||||||
|
|
||||||
|
@ProtoNumber(1)
|
||||||
|
SPANISH(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class UpdatedTitleGroup(
|
||||||
|
@ProtoNumber(1) val groupName: String,
|
||||||
|
@ProtoNumber(2) val titles: List<UpdatedTitle> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class UpdatedTitle(
|
||||||
|
@ProtoNumber(1) val title: Title? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class Chapter(
|
||||||
|
@ProtoNumber(1) val titleId: Int,
|
||||||
|
@ProtoNumber(2) val chapterId: Int,
|
||||||
|
@ProtoNumber(3) val name: String,
|
||||||
|
@ProtoNumber(4) val subTitle: String? = null,
|
||||||
|
@ProtoNumber(6) val startTimeStamp: Int,
|
||||||
|
@ProtoNumber(7) val endTimeStamp: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null)
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class MangaPage(
|
||||||
|
@ProtoNumber(1) val imageUrl: String,
|
||||||
|
@ProtoNumber(2) val width: Int,
|
||||||
|
@ProtoNumber(3) val height: Int,
|
||||||
|
@ProtoNumber(5) val encryptionKey: String? = null
|
||||||
|
)
|
15
app/src/main/java/exh/md/utils/FollowStatus.kt
Normal file
15
app/src/main/java/exh/md/utils/FollowStatus.kt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package exh.md.utils
|
||||||
|
|
||||||
|
enum class FollowStatus(val int: Int) {
|
||||||
|
UNFOLLOWED(0),
|
||||||
|
READING(1),
|
||||||
|
COMPLETED(2),
|
||||||
|
ON_HOLD(3),
|
||||||
|
PLAN_TO_READ(4),
|
||||||
|
DROPPED(5),
|
||||||
|
RE_READING(6);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int): FollowStatus? = values().find { it.int == value }
|
||||||
|
}
|
||||||
|
}
|
45
app/src/main/java/exh/md/utils/MdLang.kt
Normal file
45
app/src/main/java/exh/md/utils/MdLang.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package exh.md.utils
|
||||||
|
|
||||||
|
enum class MdLang(val lang: String, val dexLang: String, val langId: Int) {
|
||||||
|
English("en", "gb", 1),
|
||||||
|
Japanese("ja", "jp", 2),
|
||||||
|
Polish("pl", "pl", 3),
|
||||||
|
SerboCroatian("sh", "rs", 4),
|
||||||
|
Dutch("nl", "nl", 5),
|
||||||
|
Italian("it", "it", 6),
|
||||||
|
Russian("ru", "ru", 7),
|
||||||
|
German("de", "de", 8),
|
||||||
|
Hungarian("hu", "hu", 9),
|
||||||
|
French("fr", "fr", 10),
|
||||||
|
Finnish("fi", "fi", 11),
|
||||||
|
Vietnamese("vi", "vn", 12),
|
||||||
|
Greek("el", "gr", 13),
|
||||||
|
Bulgarian("bg", "bg", 14),
|
||||||
|
Spanish("es", "es", 15),
|
||||||
|
PortugeseBrazilian("pt-BR", "br", 16),
|
||||||
|
Portuguese("pt", "pt", 17),
|
||||||
|
Swedish("sv", "se", 18),
|
||||||
|
Arabic("ar", "sa", 19),
|
||||||
|
Danish("da", "dk", 20),
|
||||||
|
ChineseSimplifed("zh-Hans", "cn", 21),
|
||||||
|
Bengali("bn", "bd", 22),
|
||||||
|
Romanian("ro", "ro", 23),
|
||||||
|
Czech("cs", "cz", 24),
|
||||||
|
Mongolian("mn", "mn", 25),
|
||||||
|
Turkish("tr", "tr", 26),
|
||||||
|
Indonesian("id", "id", 27),
|
||||||
|
Korean("ko", "kr", 28),
|
||||||
|
SpanishLTAM("es-419", "mx", 29),
|
||||||
|
Persian("fa", "ir", 30),
|
||||||
|
Malay("ms", "my", 31),
|
||||||
|
Thai("th", "th", 32),
|
||||||
|
Catalan("ca", "ct", 33),
|
||||||
|
Filipino("fil", "ph", 34),
|
||||||
|
ChineseTraditional("zh-Hant", "hk", 35),
|
||||||
|
Ukrainian("uk", "ua", 36),
|
||||||
|
Burmese("my", "mm", 37),
|
||||||
|
Lithuanian("lt", "il", 38),
|
||||||
|
Hebrew("he", "il", 39),
|
||||||
|
Hindi("hi", "in", 40),
|
||||||
|
Norwegian("no", "no", 42)
|
||||||
|
}
|
248
app/src/main/java/exh/md/utils/MdUtil.kt
Normal file
248
app/src/main/java/exh/md/utils/MdUtil.kt
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
package exh.md.utils
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.jsoup.parser.Parser
|
||||||
|
|
||||||
|
class MdUtil {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org"
|
||||||
|
const val baseUrl = "https://mangadex.org"
|
||||||
|
const val randMangaPage = "/manga/"
|
||||||
|
const val apiManga = "/api/manga/"
|
||||||
|
const val apiChapter = "/api/chapter/"
|
||||||
|
const val apiChapterSuffix = "?mark_read=0"
|
||||||
|
const val groupSearchUrl = "$baseUrl/groups/0/1/"
|
||||||
|
const val followsAllApi = "/api/?type=manga_follows"
|
||||||
|
const val followsMangaApi = "/api/?type=manga_follows&manga_id="
|
||||||
|
const val coversApi = "/api/index.php?type=covers&id="
|
||||||
|
const val reportUrl = "https://api.mangadex.network/report"
|
||||||
|
const val imageUrl = "$baseUrl/data"
|
||||||
|
|
||||||
|
val jsonParser = Json {
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
allowSpecialFloatingPointValues = true
|
||||||
|
useArrayPolymorphism = true
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private const
|
||||||
|
val scanlatorSeparator = " & "
|
||||||
|
|
||||||
|
val validOneShotFinalChapters = listOf("0", "1")
|
||||||
|
|
||||||
|
val englishDescriptionTags = listOf(
|
||||||
|
"[b][u]English:",
|
||||||
|
"[b][u]English",
|
||||||
|
"[English]:",
|
||||||
|
"[B][ENG][/B]"
|
||||||
|
)
|
||||||
|
|
||||||
|
val descriptionLanguages = listOf(
|
||||||
|
"Russian / Русский",
|
||||||
|
"[u]Russian",
|
||||||
|
"[b][u]Russian",
|
||||||
|
"[RUS]",
|
||||||
|
"Russian / Русский",
|
||||||
|
"Russian/Русский:",
|
||||||
|
"Russia/Русское",
|
||||||
|
"Русский",
|
||||||
|
"RUS:",
|
||||||
|
"[b][u]German / Deutsch",
|
||||||
|
"German/Deutsch:",
|
||||||
|
"Español / Spanish",
|
||||||
|
"Spanish / Español",
|
||||||
|
"Spanish / Espa & ntilde; ol",
|
||||||
|
"Spanish / Español",
|
||||||
|
"[b][u]Spanish",
|
||||||
|
"[Español]:",
|
||||||
|
"[b] Spanish: [/ b]",
|
||||||
|
"Spanish/Español",
|
||||||
|
"Español / Spanish",
|
||||||
|
"Italian / Italiano",
|
||||||
|
"Pasta-Pizza-Mandolino/Italiano",
|
||||||
|
"Polish / polski",
|
||||||
|
"Polish / Polski",
|
||||||
|
"Polish Summary / Polski Opis",
|
||||||
|
"Polski",
|
||||||
|
"Portuguese (BR) / Português",
|
||||||
|
"Portuguese / Português",
|
||||||
|
"Português / Portuguese",
|
||||||
|
"Portuguese / Portugu",
|
||||||
|
"Portuguese / Português",
|
||||||
|
"Português",
|
||||||
|
"Portuguese (BR) / Portugu & ecirc;",
|
||||||
|
"Portuguese (BR) / Portuguê",
|
||||||
|
"[PTBR]",
|
||||||
|
"Résume Français",
|
||||||
|
"Résumé Français",
|
||||||
|
"[b][u]French",
|
||||||
|
"French / Français",
|
||||||
|
"Français",
|
||||||
|
"[hr]Fr:",
|
||||||
|
"French - Français:",
|
||||||
|
"Turkish / Türkçe",
|
||||||
|
"Turkish/Türkçe",
|
||||||
|
"[b][u]Chinese",
|
||||||
|
"Arabic / العربية",
|
||||||
|
"العربية",
|
||||||
|
"[hr]TH",
|
||||||
|
"[b][u]Vietnamese",
|
||||||
|
"[b]Links:",
|
||||||
|
"[b]Link[/b]",
|
||||||
|
"Links:",
|
||||||
|
"[b]External Links"
|
||||||
|
)
|
||||||
|
|
||||||
|
// guess the thumbnail url is .jpg this has a ~80% success rate
|
||||||
|
fun formThumbUrl(mangaUrl: String, lowQuality: Boolean): String {
|
||||||
|
var ext = ".jpg"
|
||||||
|
if (lowQuality) {
|
||||||
|
ext = ".thumb$ext"
|
||||||
|
}
|
||||||
|
return cdnUrl + "/images/manga/" + getMangaId(mangaUrl) + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ID from the manga url
|
||||||
|
fun getMangaId(url: String): String {
|
||||||
|
val lastSection = url.trimEnd('/').substringAfterLast("/")
|
||||||
|
return if (lastSection.toIntOrNull() != null) {
|
||||||
|
lastSection
|
||||||
|
} else {
|
||||||
|
// this occurs if person has manga from before that had the id/name/
|
||||||
|
url.trimEnd('/').substringBeforeLast("/").substringAfterLast("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChapterId(url: String) = url.substringBeforeLast(apiChapterSuffix).substringAfterLast("/")
|
||||||
|
|
||||||
|
// creates the manga url from the browse for the api
|
||||||
|
fun modifyMangaUrl(url: String): String =
|
||||||
|
url.replace("/title/", "/manga/").substringBeforeLast("/") + "/"
|
||||||
|
|
||||||
|
// Removes the ?timestamp from image urls
|
||||||
|
fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?")
|
||||||
|
|
||||||
|
fun cleanString(string: String): String {
|
||||||
|
val bbRegex =
|
||||||
|
"""\[(\w+)[^]]*](.*?)\[/\1]""".toRegex()
|
||||||
|
var intermediate = string
|
||||||
|
.replace("[list]", "", true)
|
||||||
|
.replace("[/list]", "", true)
|
||||||
|
.replace("[*]", "")
|
||||||
|
.replace("[hr]", "", true)
|
||||||
|
.replace("[u]", "", true)
|
||||||
|
.replace("[/u]", "", true)
|
||||||
|
.replace("[b]", "", true)
|
||||||
|
.replace("[/b]", "", true)
|
||||||
|
|
||||||
|
// Recursively remove nested bbcode
|
||||||
|
while (bbRegex.containsMatchIn(intermediate)) {
|
||||||
|
intermediate = intermediate.replace(bbRegex, "$2")
|
||||||
|
}
|
||||||
|
return Parser.unescapeEntities(intermediate, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanDescription(string: String): String {
|
||||||
|
var newDescription = string
|
||||||
|
descriptionLanguages.forEach {
|
||||||
|
newDescription = newDescription.substringBefore(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
englishDescriptionTags.forEach {
|
||||||
|
newDescription = newDescription.replace(it, "")
|
||||||
|
}
|
||||||
|
return cleanString(newDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getImageUrl(attr: String): String {
|
||||||
|
// Some images are hosted elsewhere
|
||||||
|
if (attr.startsWith("http")) {
|
||||||
|
return attr
|
||||||
|
}
|
||||||
|
return baseUrl + attr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getScanlators(scanlators: String): List<String> {
|
||||||
|
if (scanlators.isBlank()) return emptyList()
|
||||||
|
return scanlators.split(scanlatorSeparator).distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getScanlatorString(scanlators: Set<String>): String {
|
||||||
|
return scanlators.toList().sorted().joinToString(scanlatorSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
|
||||||
|
if (mangaStatus == SManga.COMPLETED) return null
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
val remove0ChaptersFromCount = chapters.distinctBy {
|
||||||
|
/*if (it.chapter_txt.isNotEmpty()) {
|
||||||
|
it.vol + it.chapter_txt
|
||||||
|
} else {*/
|
||||||
|
it.name
|
||||||
|
/*}*/
|
||||||
|
}.sortedByDescending { it.chapter_number }
|
||||||
|
|
||||||
|
remove0ChaptersFromCount.firstOrNull()?.let {
|
||||||
|
val chpNumber = floor(it.chapter_number).toInt()
|
||||||
|
val allChapters = (1..chpNumber).toMutableSet()
|
||||||
|
|
||||||
|
remove0ChaptersFromCount.forEach {
|
||||||
|
allChapters.remove(floor(it.chapter_number).toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChapters.size <= 0) return null
|
||||||
|
return allChapters.size.toString()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the chapter.
|
||||||
|
*/
|
||||||
|
fun SChapter.setMDUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getMDUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the manga.
|
||||||
|
*/
|
||||||
|
fun SManga.setMDUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getMDUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the url of the given string without the scheme and domain.
|
||||||
|
*
|
||||||
|
* @param orig the full url.
|
||||||
|
*/
|
||||||
|
private fun getMDUrlWithoutDomain(orig: String): String {
|
||||||
|
return try {
|
||||||
|
val uri = URI(orig)
|
||||||
|
var out = uri.path
|
||||||
|
if (uri.query != null) {
|
||||||
|
out += "?" + uri.query
|
||||||
|
}
|
||||||
|
if (uri.fragment != null) {
|
||||||
|
out += "#" + uri.fragment
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
orig
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package exh.metadata.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.md.utils.setMDUrlWithoutDomain
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
|
||||||
|
class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||||
|
var mdId: String? = null
|
||||||
|
|
||||||
|
var mdUrl: String? = null
|
||||||
|
|
||||||
|
var thumbnail_url: String? = null
|
||||||
|
|
||||||
|
var title: String? by titleDelegate(TITLE_TYPE_MAIN)
|
||||||
|
|
||||||
|
var description: String? = null
|
||||||
|
|
||||||
|
var author: String? = null
|
||||||
|
var artist: String? = null
|
||||||
|
|
||||||
|
var lang_flag: String? = null
|
||||||
|
|
||||||
|
var last_chapter_number: Int? = null
|
||||||
|
var rating: String? = null
|
||||||
|
var users: String? = null
|
||||||
|
|
||||||
|
var anilist_id: String? = null
|
||||||
|
var kitsu_id: String? = null
|
||||||
|
var my_anime_list_id: String? = null
|
||||||
|
var manga_updates_id: String? = null
|
||||||
|
var anime_planet_id: String? = null
|
||||||
|
|
||||||
|
var status: Int? = null
|
||||||
|
|
||||||
|
var missing_chapters: String? = null
|
||||||
|
|
||||||
|
var follow_status: Int? = null
|
||||||
|
|
||||||
|
override fun copyTo(manga: SManga) {
|
||||||
|
mdUrl?.let {
|
||||||
|
manga.url = try {
|
||||||
|
val uri = it.toUri()
|
||||||
|
val out = uri.path!!.removePrefix("/api")
|
||||||
|
out + if (out.endsWith("/")) "" else "/"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title?.let {
|
||||||
|
manga.title = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess thumbnail URL if manga does not have thumbnail URL
|
||||||
|
|
||||||
|
manga.thumbnail_url = thumbnail_url
|
||||||
|
|
||||||
|
author?.let {
|
||||||
|
manga.author = it
|
||||||
|
}
|
||||||
|
|
||||||
|
artist?.let {
|
||||||
|
manga.artist = it
|
||||||
|
}
|
||||||
|
|
||||||
|
status?.let {
|
||||||
|
manga.status = it
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
|
description?.let {
|
||||||
|
manga.description = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||||
|
val pairs = mutableListOf<Pair<String, String>>()
|
||||||
|
mdId?.let { pairs += Pair(context.getString(R.string.id), it) }
|
||||||
|
mdUrl?.let { pairs += Pair(context.getString(R.string.url), it) }
|
||||||
|
thumbnail_url?.let { pairs += Pair(context.getString(R.string.thumbnail_url), it) }
|
||||||
|
title?.let { pairs += Pair(context.getString(R.string.title), it) }
|
||||||
|
author?.let { pairs += Pair(context.getString(R.string.author), it) }
|
||||||
|
artist?.let { pairs += Pair(context.getString(R.string.artist), it) }
|
||||||
|
lang_flag?.let { pairs += Pair(context.getString(R.string.language), it) }
|
||||||
|
last_chapter_number?.let { pairs += Pair(context.getString(R.string.last_chapter_number), it.toString()) }
|
||||||
|
rating?.let { pairs += Pair(context.getString(R.string.average_rating), it) }
|
||||||
|
users?.let { pairs += Pair(context.getString(R.string.total_ratings), it) }
|
||||||
|
status?.let { pairs += Pair(context.getString(R.string.status), it.toString()) }
|
||||||
|
missing_chapters?.let { pairs += Pair(context.getString(R.string.missing_chapters), it) }
|
||||||
|
follow_status?.let { pairs += Pair(context.getString(R.string.follow_status), it.toString()) }
|
||||||
|
anilist_id?.let { pairs += Pair(context.getString(R.string.anilist_id), it) }
|
||||||
|
kitsu_id?.let { pairs += Pair(context.getString(R.string.kitsu_id), it) }
|
||||||
|
my_anime_list_id?.let { pairs += Pair(context.getString(R.string.mal_id), it) }
|
||||||
|
manga_updates_id?.let { pairs += Pair(context.getString(R.string.manga_updates_id), it) }
|
||||||
|
anime_planet_id?.let { pairs += Pair(context.getString(R.string.anime_planet_id), it) }
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TITLE_TYPE_MAIN = 0
|
||||||
|
|
||||||
|
const val TAG_TYPE_DEFAULT = 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package exh.ui.metadata.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.databinding.DescriptionAdapterMdBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||||
|
import exh.ui.metadata.MetadataViewController
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
|
import reactivecircus.flowbinding.android.view.longClicks
|
||||||
|
|
||||||
|
class MangaDexDescriptionAdapter(
|
||||||
|
private val controller: MangaController
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<MangaDexDescriptionAdapter.MangaDexDescriptionViewHolder>() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
private lateinit var binding: DescriptionAdapterMdBinding
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaDexDescriptionViewHolder {
|
||||||
|
binding = DescriptionAdapterMdBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return MangaDexDescriptionViewHolder(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MangaDexDescriptionViewHolder, position: Int) {
|
||||||
|
holder.bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MangaDexDescriptionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
fun bind() {
|
||||||
|
val meta = controller.presenter.meta
|
||||||
|
if (meta == null || meta !is MangaDexSearchMetadata) return
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
binding.mdId.text = "#" + (meta.mdId ?: 0)
|
||||||
|
|
||||||
|
val ratingFloat = meta.rating?.toFloatOrNull()?.div(2F)
|
||||||
|
val name = when (((ratingFloat ?: 100F) * 2).roundToInt()) {
|
||||||
|
0 -> R.string.rating0
|
||||||
|
1 -> R.string.rating1
|
||||||
|
2 -> R.string.rating2
|
||||||
|
3 -> R.string.rating3
|
||||||
|
4 -> R.string.rating4
|
||||||
|
5 -> R.string.rating5
|
||||||
|
6 -> R.string.rating6
|
||||||
|
7 -> R.string.rating7
|
||||||
|
8 -> R.string.rating8
|
||||||
|
9 -> R.string.rating9
|
||||||
|
10 -> R.string.rating10
|
||||||
|
else -> R.string.no_rating
|
||||||
|
}
|
||||||
|
binding.ratingBar.rating = ratingFloat ?: 0F
|
||||||
|
binding.rating.text = if (meta.users?.toIntOrNull() != null) {
|
||||||
|
itemView.context.getString(R.string.rating_view, itemView.context.getString(name), (meta.rating?.toFloatOrNull() ?: 0F).toString(), meta.users?.toIntOrNull() ?: 0)
|
||||||
|
} else {
|
||||||
|
itemView.context.getString(R.string.rating_view_no_count, itemView.context.getString(name), (meta.rating?.toFloatOrNull() ?: 0F).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
binding.mdId,
|
||||||
|
binding.rating
|
||||||
|
).forEach { textView ->
|
||||||
|
textView.longClicks()
|
||||||
|
.onEach {
|
||||||
|
itemView.context.copyToClipboard(
|
||||||
|
textView.text.toString(),
|
||||||
|
textView.text.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.moreInfo.clicks()
|
||||||
|
.onEach {
|
||||||
|
controller.router?.pushController(
|
||||||
|
MetadataViewController(
|
||||||
|
controller.manga
|
||||||
|
).withFadeTransaction()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
app/src/main/res/layout/description_adapter_md.xml
Normal file
59
app/src/main/res/layout/description_adapter_md.xml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/mdId"
|
||||||
|
style="@style/TextAppearance.Regular"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
|
||||||
|
<me.zhanghai.android.materialratingbar.MaterialRatingBar
|
||||||
|
android:id="@+id/rating_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:focusable="false"
|
||||||
|
android:isIndicator="true"
|
||||||
|
android:numStars="5"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/more_info"
|
||||||
|
style="@style/Theme.Widget.Button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/more_info"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/rating"
|
||||||
|
style="@style/TextAppearance.Regular"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/rating_bar" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -215,6 +215,11 @@
|
|||||||
<string name="az_recommends">See Recommendations</string>
|
<string name="az_recommends">See Recommendations</string>
|
||||||
<string name="merge_with_another_source">Merge With Another</string>
|
<string name="merge_with_another_source">Merge With Another</string>
|
||||||
|
|
||||||
|
<!-- Manga info fragment -->
|
||||||
|
<string name="hiatus">Hiatus</string>
|
||||||
|
<string name="cancelled">Cancelled</string>
|
||||||
|
<string name="publication_complete">Publication Complete</string>
|
||||||
|
|
||||||
<!-- Manga Info Edit -->
|
<!-- Manga Info Edit -->
|
||||||
<string name="reset_tags">Reset Tags</string>
|
<string name="reset_tags">Reset Tags</string>
|
||||||
<string name="reset_cover">Reset Cover</string>
|
<string name="reset_cover">Reset Cover</string>
|
||||||
@ -433,6 +438,15 @@
|
|||||||
<string name="rating_string">Rating string</string>
|
<string name="rating_string">Rating string</string>
|
||||||
<string name="collection">Collection</string>
|
<string name="collection">Collection</string>
|
||||||
<string name="parodies">Parodies</string>
|
<string name="parodies">Parodies</string>
|
||||||
|
<string name="author">Author</string>
|
||||||
|
<string name="last_chapter_number">Last chapter number</string>
|
||||||
|
<string name="missing_chapters">Missing chapters</string>
|
||||||
|
<string name="follow_status">Follow status</string>
|
||||||
|
<string name="anilist_id">Anilist id</string>
|
||||||
|
<string name="kitsu_id">Kitsu id</string>
|
||||||
|
<string name="mal_id">Mal id</string>
|
||||||
|
<string name="manga_updates_id">Manga updates id</string>
|
||||||
|
<string name="anime_planet_id">Anime planet id</string>
|
||||||
|
|
||||||
<!-- Extra gallery info -->
|
<!-- Extra gallery info -->
|
||||||
<plurals name="num_pages">
|
<plurals name="num_pages">
|
||||||
|
@ -46,6 +46,9 @@ buildscript {
|
|||||||
// Realm (EH)
|
// Realm (EH)
|
||||||
classpath("io.realm:realm-gradle-plugin:7.0.1")
|
classpath("io.realm:realm-gradle-plugin:7.0.1")
|
||||||
|
|
||||||
|
// SY for mangadex utils
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-serialization:${BuildPluginsVersion.KOTLIN}")
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
classpath("io.fabric.tools:gradle:1.31.0")
|
classpath("io.fabric.tools:gradle:1.31.0")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user