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.model.toDomainTrack
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -120,6 +121,10 @@ abstract class BaseTracker(
|
|||||||
updateRemote(track)
|
updateRemote(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
throw NotImplementedError("Not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||||
try {
|
try {
|
||||||
update(track)
|
update(track)
|
||||||
|
@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
|
|||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -82,4 +83,6 @@ interface Tracker {
|
|||||||
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
||||||
|
|
||||||
suspend fun setRemoteFinishDate(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.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
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 eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@ -232,6 +233,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
return api.getMangaMetadata(track)
|
||||||
|
}
|
||||||
|
|
||||||
fun saveOAuth(alOAuth: ALOAuth?) {
|
fun saveOAuth(alOAuth: ALOAuth?) {
|
||||||
trackPreferences.trackToken(this).set(json.encodeToString(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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
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.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.ALOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.json.JsonObject
|
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 {
|
private fun createDate(dateValue: Long): JsonObject {
|
||||||
if (dateValue == 0L) {
|
if (dateValue == 0L) {
|
||||||
return buildJsonObject {
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
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 eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@ -75,6 +76,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
return api.getMangaMetadata(track)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
||||||
track.copyPersonalFrom(remoteStatusTrack)
|
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.BGMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
|
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.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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -21,6 +24,7 @@ import tachiyomi.core.common.util.lang.withIOContext
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class BangumiApi(
|
class BangumiApi(
|
||||||
private val trackId: Long,
|
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 {
|
suspend fun accessToken(code: String): BGMOAuth {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
with(json) {
|
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.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
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 eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@ -139,6 +140,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
interceptor.newAuth(null)
|
interceptor.newAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||||
|
return api.getMangaMetadata(track)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getUserId(): String {
|
private fun getUserId(): String {
|
||||||
return getPassword()
|
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.KitsuAlgoliaSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
|
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.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.KitsuOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
|
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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
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 {
|
companion object {
|
||||||
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
|
||||||
private const val BASE_URL = "https://kitsu.app/api/edge/"
|
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 LOGIN_URL = "https://kitsu.app/api/oauth/token"
|
||||||
private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
|
private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
|
||||||
private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"
|
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.MURating
|
||||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
||||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
|
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.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -117,6 +119,20 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
interceptor.newAuth(authenticated.sessionToken)
|
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? {
|
fun restoreSession(): String? {
|
||||||
return trackPreferences.trackPassword(this).get().ifBlank { null }
|
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 {
|
companion object {
|
||||||
private const val BASE_URL = "https://api.mangaupdates.com"
|
private const val BASE_URL = "https://api.mangaupdates.com"
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ data class MURecord(
|
|||||||
val ratingVotes: Int? = null,
|
val ratingVotes: Int? = null,
|
||||||
@SerialName("latest_chapter")
|
@SerialName("latest_chapter")
|
||||||
val latestChapter: Int? = null,
|
val latestChapter: Int? = null,
|
||||||
|
val authors: List<MUAuthor>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
||||||
@ -36,3 +37,9 @@ fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
|||||||
start_date = this@toTrackSearch.year.toString()
|
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 android.graphics.Color
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@ -168,6 +170,21 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
|||||||
trackPreferences.trackToken(this).delete()
|
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
|
override val isLoggedIn: Boolean
|
||||||
get() = trackPreferences.trackToken(this).get().isNotEmpty()
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
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.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@ -156,6 +157,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
return api.getMangaMetadata(track)
|
||||||
|
}
|
||||||
|
|
||||||
fun getIfAuthExpired(): Boolean {
|
fun getIfAuthExpired(): Boolean {
|
||||||
return trackPreferences.trackAuthExpired(this).get()
|
return trackPreferences.trackAuthExpired(this).get()
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItem
|
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.MALListItemStatus
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga
|
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.MALOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
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 {
|
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||||
|
@ -23,4 +23,29 @@ data class MALManga(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MALMangaCovers(
|
data class MALMangaCovers(
|
||||||
val large: String = "",
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
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.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@ -98,6 +99,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||||
|
return api.getMangaMetadata(track)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||||
|
@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse
|
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.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.SMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser
|
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry
|
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 {
|
suspend fun accessToken(code: String): SMOAuth {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
with(json) {
|
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)
|
return similarHandler.getRelated(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaMetadata(track: Track): SManga? {
|
||||||
|
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val dataSaverPref = "dataSaverV5"
|
private const val dataSaverPref = "dataSaverV5"
|
||||||
|
|
||||||
|
@ -3,20 +3,26 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.children
|
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.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||||
import eu.kanade.tachiyomi.R
|
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.databinding.EditMangaDialogBinding
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||||
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
|
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
|
||||||
import exh.util.dropBlank
|
import exh.util.dropBlank
|
||||||
import exh.util.trimOrNull
|
import exh.util.trimOrNull
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.manga.model.Manga
|
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.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditMangaDialog(
|
fun EditMangaDialog(
|
||||||
@ -61,6 +79,10 @@ fun EditMangaDialog(
|
|||||||
var binding by remember {
|
var binding by remember {
|
||||||
mutableStateOf<EditMangaDialogBinding?>(null)
|
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(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
@ -109,7 +131,7 @@ fun EditMangaDialog(
|
|||||||
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
|
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
|
||||||
.also { binding = it }
|
.also { binding = it }
|
||||||
.apply {
|
.apply {
|
||||||
onViewCreated(manga, factoryContext, this, scope)
|
onViewCreated(manga, factoryContext, this, scope, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
|
||||||
}
|
}
|
||||||
.root
|
.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)
|
loadCover(manga, binding)
|
||||||
|
|
||||||
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
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.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
|
||||||
binding.resetInfo.setOnClickListener { resetInfo(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) {
|
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||||
|
@ -119,4 +119,10 @@ data class DummyTracker(
|
|||||||
track: eu.kanade.tachiyomi.data.database.models.Track,
|
track: eu.kanade.tachiyomi.data.database.models.Track,
|
||||||
epochMillis: Long,
|
epochMillis: Long,
|
||||||
) = Unit
|
) = 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> {
|
private suspend fun getSimpleChapters(manga: SManga): List<String> {
|
||||||
return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) }
|
return runCatching { service.aggregateChapters(MdUtil.getMangaId(manga.url), lang) }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
@ -129,6 +129,21 @@
|
|||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="12dp" />
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
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/fill_from_tracker"
|
||||||
|
android:textAllCaps="false" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/reset_tags"
|
android:id="@+id/reset_tags"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -148,6 +163,7 @@
|
|||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/reset_info"
|
android:text="@string/reset_info"
|
||||||
android:textAllCaps="false" />
|
android:textAllCaps="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -403,6 +403,9 @@
|
|||||||
<string name="artist_hint">Artist: %1$s</string>
|
<string name="artist_hint">Artist: %1$s</string>
|
||||||
<string name="thumbnail_url_hint">Thumbnail Url: %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="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 -->
|
<!-- Browse -->
|
||||||
<!-- Sources Tab -->
|
<!-- Sources Tab -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user