Get manga info from tracker (#1271)
* Barebones setup (only AniList works) * Show tracker selection dialog when entry has more than one tracker * MangaUpdates implementation * Add logging and toast on error. * MyAnimeList implementation * Kitsu implementation * Fix MAL authors and artists * Decode AL description * Throw NotImplementedError instead of returning null * Use logcat from LogcatExtensions * Replace strings with MR strings * Missed a string * Delete unused Author class. * Add Bangumi & Shikimori support for info edit (#2) This adds the necessary API calls and DTOs to allow for editing an entry's data to the data from a tracker, specifically adding support for Bangumi and Shikimori. * Exclude enhanced trackers from tracker select dialog * MdList implementation * Remember getTracks and trackerManager Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com> --------- Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
parent
34e9d9f146
commit
fd120c5081
@ -6,6 +6,7 @@ import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -120,6 +121,10 @@ abstract class BaseTracker(
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
throw NotImplementedError("Not implemented.")
|
||||
}
|
||||
|
||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
|
@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -82,4 +83,6 @@ interface Tracker {
|
||||
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@ -232,6 +233,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
fun saveOAuth(alOAuth: ALOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
|
||||
}
|
||||
|
@ -5,15 +5,18 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@ -288,6 +291,71 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}mangaId: Int!) {
|
||||
|Media (id: ${'$'}mangaId) {
|
||||
|id
|
||||
|title {
|
||||
|userPreferred
|
||||
|}
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
|description
|
||||
|staff {
|
||||
|edges {
|
||||
|role
|
||||
|node {
|
||||
|name {
|
||||
|userPreferred
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", track.remoteId)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<ALMangaMetadata>()
|
||||
.let {
|
||||
val media = it.data.media
|
||||
TrackMangaMetadata(
|
||||
remoteId = media.id,
|
||||
title = media.title.userPreferred,
|
||||
thumbnailUrl = media.coverImage.large,
|
||||
description = media.description?.htmlDecode()?.ifEmpty { null },
|
||||
authors = media.staff.edges
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { it.node.name.userPreferred }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = media.staff.edges
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { it.node.name.userPreferred }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDate(dateValue: Long): JsonObject {
|
||||
if (dateValue == 0L) {
|
||||
return buildJsonObject {
|
||||
|
@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadata(
|
||||
val data: ALMangaMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadataData(
|
||||
@SerialName("Media")
|
||||
val media: ALMangaMetadataMedia,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadataMedia(
|
||||
val id: Long,
|
||||
val title: ALItemTitle,
|
||||
val coverImage: ItemCover,
|
||||
val description: String?,
|
||||
val staff: ALStaff,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaff(
|
||||
val edges: List<ALStaffEdge>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffEdge(
|
||||
val role: String,
|
||||
val node: ALStaffNode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffNode(
|
||||
val name: ALItemTitle,
|
||||
)
|
@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -75,6 +76,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
||||
track.copyPersonalFrom(remoteStatusTrack)
|
||||
|
@ -7,6 +7,9 @@ import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
@ -21,6 +24,7 @@ import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
class BangumiApi(
|
||||
private val trackId: Long,
|
||||
@ -127,6 +131,34 @@ class BangumiApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("${API_URL}/v0/subjects/${track.remoteId}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSubject>()
|
||||
.let {
|
||||
TrackMangaMetadata(
|
||||
remoteId = it.id,
|
||||
title = it.nameCn,
|
||||
thumbnailUrl = it.images?.common,
|
||||
description = it.summary,
|
||||
authors = it.infobox
|
||||
.filter { it.key == "作者" }
|
||||
.filterIsInstance<Infobox.SingleValue>()
|
||||
.map { it.value }
|
||||
.joinToString(", "),
|
||||
artists = it.infobox
|
||||
.filter { it.key == "插图" }
|
||||
.filterIsInstance<Infobox.SingleValue>()
|
||||
.map { it.value }
|
||||
.joinToString(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): BGMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
|
@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class BGMSubject(
|
||||
val images: BGMSearchItemCovers?,
|
||||
val summary: String,
|
||||
val name: String,
|
||||
@SerialName("name_cn")
|
||||
val nameCn: String,
|
||||
val infobox: List<Infobox>,
|
||||
val id: Long,
|
||||
)
|
||||
|
||||
// infobox deserializer and related classes courtesy of
|
||||
// https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89
|
||||
object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) {
|
||||
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Infobox> {
|
||||
if (element !is JsonObject) throw SerializationException("Expected JsonObject go ${element::class}")
|
||||
val value = element["value"]
|
||||
|
||||
return when (value) {
|
||||
is JsonArray -> Infobox.MultipleValues.serializer()
|
||||
is JsonPrimitive -> Infobox.SingleValue.serializer()
|
||||
else -> throw SerializationException("Unexpected element type ${element::class}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = InfoBoxSerializer::class)
|
||||
sealed interface Infobox {
|
||||
val key: String
|
||||
|
||||
@Serializable
|
||||
class SingleValue(
|
||||
override val key: String,
|
||||
val value: String,
|
||||
) : Infobox
|
||||
|
||||
@Serializable
|
||||
class MultipleValues(
|
||||
override val key: String,
|
||||
val value: List<InfoboxNestedValue>,
|
||||
) : Infobox
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class InfoboxNestedValue(
|
||||
@SerialName("k")
|
||||
val key: String? = null,
|
||||
@SerialName("v")
|
||||
val value: String,
|
||||
)
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -139,6 +140,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
private fun getUserId(): String {
|
||||
return getPassword()
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAlgoliaSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -15,6 +17,7 @@ import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
@ -240,11 +243,80 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query(${'$'}libraryId: ID!, ${'$'}staffCount: Int) {
|
||||
|findLibraryEntryById(id: ${'$'}libraryId) {
|
||||
|media {
|
||||
|id
|
||||
|titles {
|
||||
|preferred
|
||||
|}
|
||||
|posterImage {
|
||||
|original {
|
||||
|url
|
||||
|}
|
||||
|}
|
||||
|description
|
||||
|staff(first: ${'$'}staffCount) {
|
||||
|nodes {
|
||||
|role
|
||||
|person {
|
||||
|name
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("libraryId", track.remoteId)
|
||||
put("staffCount", 25) // 25 based on nothing
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
GRAPHQL_URL,
|
||||
headers = headersOf("Accept-Language", "en"),
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuMangaMetadata>()
|
||||
.let {
|
||||
val manga = it.data.findLibraryEntryById.media
|
||||
TrackMangaMetadata(
|
||||
remoteId = manga.id.toLong(),
|
||||
title = manga.titles.preferred,
|
||||
thumbnailUrl = manga.posterImage.original.url,
|
||||
description = manga.description.en?.htmlDecode()?.ifEmpty { null },
|
||||
authors = manga.staff.nodes
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = manga.staff.nodes
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
|
||||
private const val BASE_URL = "https://kitsu.app/api/edge/"
|
||||
private const val GRAPHQL_URL = "https://kitsu.app/api/graphql"
|
||||
private const val LOGIN_URL = "https://kitsu.app/api/oauth/token"
|
||||
private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
|
||||
private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"
|
||||
|
@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadata(
|
||||
val data: KitsuMangaMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataData(
|
||||
val findLibraryEntryById: KitsuMangaMetadataById,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataById(
|
||||
val media: KitsuMangaMetadataMedia,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataMedia(
|
||||
val id: String,
|
||||
val titles: KitsuMangaTitle,
|
||||
val posterImage: KitsuMangaCover,
|
||||
val description: KitsuMangaDescription,
|
||||
val staff: KitsuMangaStaff,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaTitle(
|
||||
val preferred: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaCover(
|
||||
val original: KitsuMangaCoverUrl,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaCoverUrl(
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaDescription(
|
||||
val en: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaff(
|
||||
val nodes: List<KitsuMangaStaffNode>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaffNode(
|
||||
val role: String,
|
||||
val person: KitsuMangaStaffPerson,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaffPerson(
|
||||
val name: String,
|
||||
)
|
@ -10,7 +10,9 @@ import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.i18n.MR
|
||||
@ -117,6 +119,20 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
||||
interceptor.newAuth(authenticated.sessionToken)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
val series = api.getSeries(track)
|
||||
return series?.let {
|
||||
TrackMangaMetadata(
|
||||
it.seriesId,
|
||||
it.title?.htmlDecode(),
|
||||
it.image?.url?.original,
|
||||
it.description?.htmlDecode(),
|
||||
it.authors?.filter { it.type == "Author" }?.joinToString(separator = ", ") { it.name ?: "" },
|
||||
it.authors?.filter { it.type == "Artist" }?.joinToString(separator = ", ") { it.name ?: "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreSession(): String? {
|
||||
return trackPreferences.trackPassword(this).get().ifBlank { null }
|
||||
}
|
||||
|
@ -190,6 +190,14 @@ class MangaUpdatesApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSeries(track: DomainTrack): MURecord {
|
||||
return with(json) {
|
||||
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<MURecord>()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mangaupdates.com"
|
||||
|
||||
|
@ -21,6 +21,7 @@ data class MURecord(
|
||||
val ratingVotes: Int? = null,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int? = null,
|
||||
val authors: List<MUAuthor>? = null,
|
||||
)
|
||||
|
||||
fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
||||
@ -36,3 +37,9 @@ fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
||||
start_date = this@toTrackSearch.year.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MUAuthor(
|
||||
val type: String? = null,
|
||||
val name: String? = null,
|
||||
)
|
||||
|
@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.track.mdlist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@ -168,6 +170,21 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
trackPreferences.trackToken(this).delete()
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val manga = mdex.getMangaMetadata(track.toDbTrack())
|
||||
TrackMangaMetadata(
|
||||
remoteId = 0,
|
||||
title = manga?.title,
|
||||
thumbnailUrl = manga?.thumbnail_url, // Doesn't load the actual cover because of Refer header
|
||||
description = manga?.description,
|
||||
authors = manga?.author,
|
||||
artists = manga?.artist,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val isLoggedIn: Boolean
|
||||
get() = trackPreferences.trackToken(this).get().isNotEmpty()
|
||||
|
||||
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.track.model
|
||||
|
||||
data class TrackMangaMetadata(
|
||||
val remoteId: Long? = null,
|
||||
val title: String? = null,
|
||||
val thumbnailUrl: String? = null,
|
||||
val description: String? = null,
|
||||
val authors: String? = null,
|
||||
val artists: String? = null,
|
||||
)
|
@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@ -156,6 +157,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
fun getIfAuthExpired(): Boolean {
|
||||
return trackPreferences.trackAuthExpired(this).get()
|
||||
}
|
||||
|
@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItem
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||
@ -193,6 +195,41 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendPath(track.remoteId.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,main_picture,authors{first_name,last_name}",
|
||||
)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALMangaMetadata>()
|
||||
.let {
|
||||
TrackMangaMetadata(
|
||||
remoteId = it.id,
|
||||
title = it.title,
|
||||
thumbnailUrl = it.covers.large.ifEmpty { null } ?: it.covers.medium,
|
||||
description = it.synopsis,
|
||||
authors = it.authors
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
artists = it.authors
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
||||
return withIOContext {
|
||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||
|
@ -23,4 +23,29 @@ data class MALManga(
|
||||
@Serializable
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
val medium: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALMangaMetadata(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val synopsis: String?,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val authors: List<MALAuthor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val node: MALAuthorNode,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@ -98,6 +99,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
|
@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMManga
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMMetadata
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry
|
||||
@ -132,6 +134,65 @@ class ShikimoriApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query(${'$'}ids: String!) {
|
||||
|mangas(ids: ${'$'}ids) {
|
||||
|id
|
||||
|name
|
||||
|description
|
||||
|poster {
|
||||
|originalUrl
|
||||
|}
|
||||
|personRoles {
|
||||
|person {
|
||||
|name
|
||||
|}
|
||||
|rolesEn
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("ids", "${track.remoteId}")
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"https://shikimori.one/api/graphql",
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<SMMetadata>()
|
||||
.let {
|
||||
if (it.data.mangas.isEmpty()) throw Exception("Could not get metadata from Shikimori")
|
||||
val manga = it.data.mangas[0]
|
||||
TrackMangaMetadata(
|
||||
remoteId = manga.id.toLong(),
|
||||
title = manga.name,
|
||||
thumbnailUrl = manga.poster.originalUrl,
|
||||
description = manga.description,
|
||||
authors = manga.personRoles
|
||||
.filter { it.rolesEn.contains("Story") || it.rolesEn.contains("Story & Art") }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = manga.personRoles
|
||||
.filter { it.rolesEn.contains("Art") || it.rolesEn.contains("Story & Art") }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): SMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
|
@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SMMetadata(
|
||||
val data: SMMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMetadataData(
|
||||
val mangas: List<SMMetadataResult>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMetadataResult(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val poster: SMMangaPoster,
|
||||
val personRoles: List<SMMangaPersonRoles>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMangaPoster(
|
||||
val originalUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMangaPersonRoles(
|
||||
val person: SMPerson,
|
||||
val rolesEn: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMPerson(
|
||||
val name: String,
|
||||
)
|
@ -313,6 +313,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return similarHandler.getRelated(manga)
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: Track): SManga? {
|
||||
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val dataSaverPref = "dataSaverV5"
|
||||
|
||||
|
@ -3,20 +3,26 @@ package eu.kanade.tachiyomi.ui.manga
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
@ -26,22 +32,34 @@ import coil3.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun EditMangaDialog(
|
||||
@ -61,6 +79,10 @@ fun EditMangaDialog(
|
||||
var binding by remember {
|
||||
mutableStateOf<EditMangaDialogBinding?>(null)
|
||||
}
|
||||
val showTrackerSelectionDialogue = remember { mutableStateOf(false) }
|
||||
val getTracks = remember { Injekt.get<GetTracks>() }
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
val tracks = remember { mutableStateOf(emptyList<Pair<Track, Tracker>>()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
@ -109,7 +131,7 @@ fun EditMangaDialog(
|
||||
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
|
||||
.also { binding = it }
|
||||
.apply {
|
||||
onViewCreated(manga, factoryContext, this, scope)
|
||||
onViewCreated(manga, factoryContext, this, scope, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
|
||||
}
|
||||
.root
|
||||
},
|
||||
@ -118,9 +140,61 @@ fun EditMangaDialog(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (showTrackerSelectionDialogue.value) {
|
||||
TrackerSelectDialog(
|
||||
tracks = tracks.value,
|
||||
onDismissRequest = { showTrackerSelectionDialogue.value = false },
|
||||
onTrackerSelect = { tracker, track ->
|
||||
scope.launch {
|
||||
autofillFromTracker(binding!!, track, tracker)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
@Composable
|
||||
private fun TrackerSelectDialog(
|
||||
tracks: List<Pair<Track, Tracker>>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onTrackerSelect: (
|
||||
tracker: Tracker,
|
||||
track: Track,
|
||||
) -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(SYMR.strings.select_tracker))
|
||||
},
|
||||
text = {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
tracks.forEach { (track, tracker) ->
|
||||
TrackLogoIcon(
|
||||
tracker,
|
||||
onClick = {
|
||||
onTrackerSelect(tracker, track)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
|
||||
loadCover(manga, binding)
|
||||
|
||||
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||
@ -203,6 +277,55 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
||||
|
||||
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
|
||||
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) }
|
||||
binding.autofillFromTracker.setOnClickListener {
|
||||
scope.launch {
|
||||
getTrackers(manga, binding, context, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTrackers(manga: Manga, binding: EditMangaDialogBinding, context: Context, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
|
||||
tracks.value = getTracks.await(manga.id).map { track ->
|
||||
track to trackerManager.get(track.trackerId)!!
|
||||
}
|
||||
.filterNot { (_, tracker) -> tracker is EnhancedTracker }
|
||||
|
||||
if (tracks.value.isEmpty()) {
|
||||
context.toast(context.stringResource(SYMR.strings.entry_not_tracked))
|
||||
return
|
||||
}
|
||||
|
||||
if (tracks.value.size > 1) {
|
||||
showTrackerSelectionDialogue.value = true
|
||||
return
|
||||
}
|
||||
|
||||
autofillFromTracker(binding, tracks.value.first().first, tracks.value.first().second)
|
||||
}
|
||||
|
||||
private fun setTextIfNotBlank(field: (String) -> Unit, value: String?) {
|
||||
value?.takeIf { it.isNotBlank() }?.let { field(it) }
|
||||
}
|
||||
|
||||
private suspend fun autofillFromTracker(binding: EditMangaDialogBinding, track: Track, tracker: Tracker) {
|
||||
try {
|
||||
val trackerMangaMetadata = tracker.getMangaMetadata(track)
|
||||
|
||||
setTextIfNotBlank(binding.title::setText, trackerMangaMetadata?.title)
|
||||
setTextIfNotBlank(binding.mangaAuthor::setText, trackerMangaMetadata?.authors)
|
||||
setTextIfNotBlank(binding.mangaArtist::setText, trackerMangaMetadata?.artists)
|
||||
setTextIfNotBlank(binding.thumbnailUrl::setText, trackerMangaMetadata?.thumbnailUrl)
|
||||
setTextIfNotBlank(binding.mangaDescription::setText, trackerMangaMetadata?.description)
|
||||
} catch (e: Throwable) {
|
||||
tracker.logcat(LogPriority.ERROR, e)
|
||||
binding.root.context.toast(
|
||||
binding.root.context.stringResource(
|
||||
MR.strings.track_error,
|
||||
tracker.name,
|
||||
e.message ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
|
@ -119,4 +119,10 @@ data class DummyTracker(
|
||||
track: eu.kanade.tachiyomi.data.database.models.Track,
|
||||
epochMillis: Long,
|
||||
) = Unit
|
||||
|
||||
override suspend fun getMangaMetadata(
|
||||
track: tachiyomi.domain.track.model.Track,
|
||||
): eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata = eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata(
|
||||
0, "test", "test", "test", "test", "test",
|
||||
)
|
||||
}
|
||||
|
@ -128,6 +128,36 @@ class MangaHandler(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(
|
||||
track: Track,
|
||||
sourceId: Long,
|
||||
coverQuality: String,
|
||||
tryUsingFirstVolumeCover: Boolean,
|
||||
altTitlesInDesc: Boolean,
|
||||
): SManga? {
|
||||
return withIOContext {
|
||||
val mangaId = MdUtil.getMangaId(track.tracking_url)
|
||||
val response = service.viewManga(mangaId)
|
||||
val coverFileName = if (tryUsingFirstVolumeCover) {
|
||||
service.fetchFirstVolumeCover(response)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
apiMangaParser.parseToManga(
|
||||
SManga.create().apply {
|
||||
url = track.tracking_url
|
||||
},
|
||||
sourceId,
|
||||
response,
|
||||
emptyList(),
|
||||
null,
|
||||
coverFileName,
|
||||
coverQuality,
|
||||
altTitlesInDesc,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSimpleChapters(manga: SManga): List<String> {
|
||||
return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) }
|
||||
.onFailure {
|
||||
|
@ -130,24 +130,40 @@
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_tags"
|
||||
android:id="@+id/autofill_from_tracker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/reset_tags"
|
||||
android:text="@string/fill_from_tracker"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_info"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/reset_info"
|
||||
android:textAllCaps="false" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/reset_tags"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/reset_info"
|
||||
android:textAllCaps="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
@ -403,6 +403,9 @@
|
||||
<string name="artist_hint">Artist: %1$s</string>
|
||||
<string name="thumbnail_url_hint">Thumbnail Url: %1$s</string>
|
||||
<string name="multi_tags_comma_separated">Enter tag(s), seperated by commas.</string>
|
||||
<string name="select_tracker">Select a tracker</string>
|
||||
<string name="entry_not_tracked">Entry is not tracked.</string>
|
||||
<string name="fill_from_tracker">Fill from tracker</string>
|
||||
|
||||
<!-- Browse -->
|
||||
<!-- Sources Tab -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user