feat(MangaDex): use tracker links to associate mangas automatically with trackers (#991)
* feat(MangaDex): use tracker links to associate mangas automatically with trackers (jobobby04/TachiyomiSY#1387) * feat: add searchById support to trackers (MAL, AniList, MangaUpdates only) * feat: add new preference to toggle auto selection of tracker items using source metadata if available * feat: add new preference to toggle auto selection of tracker items using source metadata if available * feat: add automatic title selection using source metadata to TrackInfoDialog.kt * style: apply spotless * refactor: remove hardcoded MangaDexSearchMetadata cast and introduce common interface (cherry picked from commit 217503eab0e2fe551943e3b702cc6df4aaae8ebf) * Handle null when searchId failed * Wrap the await() call in a use block to ensure the OkHttp Response is properly closed after use * Simplify Regex * Injection via construction * Wrap the blocking HTTP call in an IO dispatcher context --------- Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
This commit is contained in:
parent
2661122d9a
commit
cd3dc79583
15 changed files with 375 additions and 77 deletions
|
|
@ -43,6 +43,13 @@ class TrackPreferences(
|
|||
AutoTrackState.ALWAYS,
|
||||
)
|
||||
|
||||
// SY -->
|
||||
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
|
||||
"pref_resolve_using_source_metadata_key",
|
||||
true,
|
||||
)
|
||||
// SY <--
|
||||
|
||||
// KMK -->
|
||||
fun autoSyncProgressFromTrackers() = preferenceStore.getBoolean("pref_auto_sync_progress_from_trackers_key", true)
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import tachiyomi.core.common.util.lang.withUIContext
|
|||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
|
@ -146,6 +147,13 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||
title = stringResource(KMR.strings.pref_auto_sync_progress_from_trackers),
|
||||
),
|
||||
// KMK <--
|
||||
// SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = trackPreferences.resolveUsingSourceMetadata(),
|
||||
title = stringResource(SYMR.strings.pref_tracker_resolve_using_source_metadata),
|
||||
subtitle = stringResource(SYMR.strings.pref_tracker_resolve_using_source_metadata_summary),
|
||||
),
|
||||
// SY <--
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = persistentListOf(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -132,6 +133,12 @@ abstract class BaseTracker(
|
|||
throw NotImplementedError("Not implemented.")
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override suspend fun searchById(id: String): TrackSearch? {
|
||||
throw NotImplementedError("Not implemented.")
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ interface Tracker {
|
|||
|
||||
// SY -->
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata
|
||||
|
||||
suspend fun searchById(id: String): TrackSearch?
|
||||
// SY <--
|
||||
|
||||
// KMK -->
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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 exh.log.xLogW
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -238,6 +239,17 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override suspend fun searchById(id: String): TrackSearch? {
|
||||
return try {
|
||||
api.searchById(id)
|
||||
} catch (e: Exception) {
|
||||
xLogW("Error during searchById '$id': ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun saveOAuth(alOAuth: ALOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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.ALIdSearchResult
|
||||
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
|
||||
|
|
@ -388,6 +389,56 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
suspend fun searchById(id: String): TrackSearch {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}mangaId: Int!) {
|
||||
|Media (id: ${'$'}mangaId) {
|
||||
|id
|
||||
|title {
|
||||
|userPreferred
|
||||
|}
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
|format
|
||||
|status
|
||||
|chapters
|
||||
|description
|
||||
|startDate {
|
||||
|year
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|averageScore
|
||||
|}
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", id)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<ALIdSearchResult>()
|
||||
.data.media
|
||||
.toALManga()
|
||||
.toTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun createDate(dateValue: Long): JsonObject {
|
||||
if (dateValue == 0L) {
|
||||
return buildJsonObject {
|
||||
|
|
|
|||
|
|
@ -18,3 +18,16 @@ data class ALSearchPage(
|
|||
data class ALSearchMedia(
|
||||
val media: List<ALSearchItem>,
|
||||
)
|
||||
|
||||
// SY -->
|
||||
@Serializable
|
||||
data class ALIdSearchResult(
|
||||
val data: ALIdSearchMedia,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALIdSearchMedia(
|
||||
@SerialName("Media")
|
||||
val media: ALSearchItem,
|
||||
)
|
||||
// SY <--
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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 exh.log.xLogW
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.i18n.MR
|
||||
|
|
@ -135,6 +136,34 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override suspend fun searchById(id: String): TrackSearch? {
|
||||
/*
|
||||
* MangaUpdates uses newer base36 IDs (in URLs displayed as an encoded string, internally as a long)
|
||||
* as well as older sequential numeric IDs, which were phased out to prevent heavy load caused by
|
||||
* database scraping. Unfortunately, sites like MD sometimes still provides links with the old IDs,
|
||||
* so we need to convert them.
|
||||
* Because the API only accepts the newer IDs, we are forced to access the legacy non-API website
|
||||
* (ex. https://www.mangaupdates.com/series.html?id=15), which is a permanent redirect (HTTP 308) to the new one.
|
||||
*/
|
||||
|
||||
val base36Id = if (id.matches(Regex("""^\d+$"""))) {
|
||||
api.convertToNewId(id.toInt()) ?: return null
|
||||
} else {
|
||||
id
|
||||
}
|
||||
|
||||
return try {
|
||||
base36Id.toLong(36).let { longId ->
|
||||
api.getSeries(longId).toTrackSearch(this.id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
xLogW("Error during searchById '$id': ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun restoreSession(): String? {
|
||||
return trackPreferences.trackPassword(this).get().ifBlank { null }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.network.DELETE
|
|||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -25,6 +26,8 @@ import kotlinx.serialization.json.putJsonObject
|
|||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
|
|
@ -190,14 +193,38 @@ class MangaUpdatesApi(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getSeries(track: DomainTrack): MURecord {
|
||||
suspend fun getSeries(track: DomainTrack): MURecord =
|
||||
getSeries(track.remoteId)
|
||||
|
||||
// SY -->
|
||||
suspend fun getSeries(remoteId: Long): MURecord {
|
||||
return with(json) {
|
||||
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}"))
|
||||
client.newCall(GET("$BASE_URL/v1/series/$remoteId"))
|
||||
.awaitSuccess()
|
||||
.parseAs<MURecord>()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun convertToNewId(legacyId: Int): String? = withIOContext {
|
||||
client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
.newCall(GET("https://www.mangaupdates.com/series.html?id=$legacyId"))
|
||||
.await()
|
||||
.use { response ->
|
||||
response.takeIf(Response::isRedirect)
|
||||
?.header("Location")
|
||||
?.let { location ->
|
||||
// Extract the new id from the redirected URL
|
||||
Regex("""/series/(\w+)""")
|
||||
.find(location)
|
||||
?.groups?.get(1)
|
||||
?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mangaupdates.com"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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 exh.log.xLogW
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -160,6 +161,22 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override suspend fun searchById(id: String): TrackSearch? {
|
||||
val searchId = id.toIntOrNull()
|
||||
?: run {
|
||||
xLogW("Invalid ID format for searchById: $id")
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
api.getMangaDetails(searchId)
|
||||
} catch (e: Exception) {
|
||||
xLogW("Error during searchById '$id': ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun getIfAuthExpired(): Boolean {
|
||||
return trackPreferences.trackAuthExpired(this).get()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,20 @@ package eu.kanade.tachiyomi.ui.manga.track
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -30,6 +33,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
|
|
@ -40,6 +45,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.track.TrackChapterSelector
|
||||
import eu.kanade.presentation.track.TrackDateSelector
|
||||
|
|
@ -53,10 +59,13 @@ 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.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.metadata.metadata.base.TrackerIdMetadata
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
@ -71,6 +80,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
|||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.DeleteTrack
|
||||
|
|
@ -103,78 +113,95 @@ data class TrackInfoDialogHomeScreen(
|
|||
val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
TrackInfoDialogHome(
|
||||
trackItems = state.trackItems,
|
||||
dateFormat = dateFormat,
|
||||
onStatusClick = {
|
||||
navigator.push(
|
||||
TrackStatusSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onChapterClick = {
|
||||
navigator.push(
|
||||
TrackChapterSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onScoreClick = {
|
||||
navigator.push(
|
||||
TrackScoreSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onStartDateEdit = {
|
||||
navigator.push(
|
||||
TrackDateSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
start = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onEndDateEdit = {
|
||||
navigator.push(
|
||||
TrackDateSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
start = false,
|
||||
),
|
||||
)
|
||||
},
|
||||
onNewSearch = {
|
||||
if (it.tracker is EnhancedTracker) {
|
||||
screenModel.registerEnhancedTracking(it)
|
||||
} else {
|
||||
navigator.push(
|
||||
TrackerSearchScreen(
|
||||
mangaId = mangaId,
|
||||
initialQuery = it.track?.title ?: mangaTitle,
|
||||
currentUrl = it.track?.remoteUrl,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
// SY -->
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
if (state.isLoading) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp)
|
||||
.windowInsetsPadding(WindowInsets.systemBars),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
stringResource(MR.strings.loading),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = { openTrackerInBrowser(context, it) },
|
||||
onRemoved = {
|
||||
navigator.push(
|
||||
TrackerRemoveScreen(
|
||||
mangaId = mangaId,
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
}
|
||||
// SY <--
|
||||
else {
|
||||
TrackInfoDialogHome(
|
||||
trackItems = state.trackItems,
|
||||
dateFormat = dateFormat,
|
||||
onStatusClick = {
|
||||
navigator.push(
|
||||
TrackStatusSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onChapterClick = {
|
||||
navigator.push(
|
||||
TrackChapterSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onScoreClick = {
|
||||
navigator.push(
|
||||
TrackScoreSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onStartDateEdit = {
|
||||
navigator.push(
|
||||
TrackDateSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
start = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onEndDateEdit = {
|
||||
navigator.push(
|
||||
TrackDateSelectorScreen(
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
start = false,
|
||||
),
|
||||
)
|
||||
},
|
||||
onNewSearch = {
|
||||
if (it.tracker is EnhancedTracker) {
|
||||
screenModel.registerEnhancedTracking(it)
|
||||
} else {
|
||||
// SY -->
|
||||
screenModel.newSearch(navigator, it, mangaTitle)
|
||||
// SY <--
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = { openTrackerInBrowser(context, it) },
|
||||
onRemoved = {
|
||||
navigator.push(
|
||||
TrackerRemoveScreen(
|
||||
mangaId = mangaId,
|
||||
track = it.track!!,
|
||||
serviceId = it.tracker.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onCopyLink = { context.copyTrackerLink(it) },
|
||||
onTogglePrivate = screenModel::togglePrivate,
|
||||
)
|
||||
},
|
||||
onCopyLink = { context.copyTrackerLink(it) },
|
||||
onTogglePrivate = screenModel::togglePrivate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -198,6 +225,14 @@ data class TrackInfoDialogHomeScreen(
|
|||
private val mangaId: Long,
|
||||
private val sourceId: Long,
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
// SY -->
|
||||
private val trackerManager: TrackerManager = Injekt.get(),
|
||||
private val trackPreferences: TrackPreferences = Injekt.get(),
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val sourceManager: SourceManager = Injekt.get<SourceManager>(),
|
||||
val getFlatMetadataById: GetFlatMetadataById = Injekt.get<GetFlatMetadataById>(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<Model.State>(State()) {
|
||||
|
||||
init {
|
||||
|
|
@ -227,6 +262,77 @@ data class TrackInfoDialogHomeScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun newSearch(navigator: Navigator, item: TrackItem, mangaTitle: String) {
|
||||
screenModelScope.launchNonCancellable {
|
||||
if (trackPreferences.resolveUsingSourceMetadata().get()) {
|
||||
// Check if the tracker id is contained in the metadata
|
||||
val result = getTrackerIdFromMetadata(item.tracker.id)
|
||||
if (result != null) {
|
||||
mutableState.update { it.copy(isLoading = true) }
|
||||
|
||||
// Try to register tracking by id
|
||||
val success = registerTrackingById(item.tracker.id, result)
|
||||
|
||||
mutableState.update { it.copy(isLoading = false) }
|
||||
|
||||
if (success) {
|
||||
// Return on success
|
||||
return@launchNonCancellable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open search screen
|
||||
navigator.push(
|
||||
TrackerSearchScreen(
|
||||
mangaId = mangaId,
|
||||
initialQuery = item.track?.title ?: mangaTitle,
|
||||
currentUrl = item.track?.remoteUrl,
|
||||
serviceId = item.tracker.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTrackerIdFromMetadata(trackerId: Long): String? {
|
||||
try {
|
||||
val metadataSource = sourceManager.get(sourceId)
|
||||
?.getMainSource<MetadataSource<*, *>>() ?: return null
|
||||
|
||||
return getFlatMetadataById.await(mangaId)?.run {
|
||||
// Use 'raise' to dynamically obtain the specific metadata type and then attempt to cast
|
||||
raise(metadataSource.metaClass) as? TrackerIdMetadata
|
||||
}?.let { metadata ->
|
||||
when (trackerId) {
|
||||
trackerManager.aniList.id -> metadata.anilistId
|
||||
trackerManager.kitsu.id -> metadata.kitsuId
|
||||
trackerManager.myAnimeList.id -> metadata.myAnimeListId
|
||||
trackerManager.mangaUpdates.id -> metadata.mangaUpdatesId
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to get tracker ID from metadata" }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerTrackingById(trackerId: Long, remoteId: String): Boolean {
|
||||
trackerManager.get(trackerId)?.let { tracker ->
|
||||
try {
|
||||
tracker.searchById(remoteId)?.let { track ->
|
||||
tracker.register(track, mangaId)
|
||||
return true
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to register tracking by id" }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private suspend fun refreshTrackers() {
|
||||
val refreshTracks = Injekt.get<RefreshTracks>()
|
||||
val context = Injekt.get<Application>()
|
||||
|
|
@ -268,6 +374,9 @@ data class TrackInfoDialogHomeScreen(
|
|||
@Immutable
|
||||
data class State(
|
||||
val trackItems: List<TrackItem> = emptyList(),
|
||||
// SY -->
|
||||
val isLoading: Boolean = false,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ data class DummyTracker(
|
|||
override suspend fun getMangaMetadata(
|
||||
track: Track,
|
||||
): TrackMangaMetadata = TrackMangaMetadata(0, "test", "test", "test", "test", "test")
|
||||
|
||||
override suspend fun searchById(id: String) = null
|
||||
// SY <--
|
||||
|
||||
// KMK -->
|
||||
|
|
|
|||
|
|
@ -176,6 +176,10 @@
|
|||
<string name="pref_hide_history_button">Show history in the nav</string>
|
||||
<string name="pref_show_bottom_bar_labels">Always show nav labels</string>
|
||||
|
||||
<!-- Tracker settings -->
|
||||
<string name="pref_tracker_resolve_using_source_metadata">Select entries using source metadata</string>
|
||||
<string name="pref_tracker_resolve_using_source_metadata_summary">Automatically selects the matching title if the source provides links to trackers. Currently supported by MangaDex</string>
|
||||
|
||||
<!-- Library settings -->
|
||||
<string name="pref_sorting_settings">Sorting Settings</string>
|
||||
<string name="pref_skip_pre_migration_summary">Use last saved pre-migration preferences and sources to mass migrate</string>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import android.content.Context
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.md.utils.MangaDexRelation
|
||||
import exh.metadata.metadata.base.TrackerIdMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
||||
@Serializable
|
||||
class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||
class MangaDexSearchMetadata : RaisedSearchMetadata(), TrackerIdMetadata {
|
||||
var mdUuid: String? = null
|
||||
|
||||
// var mdUrl: String? = null
|
||||
|
|
@ -31,11 +32,11 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
|||
var rating: Float? = null
|
||||
// var users: String? = null
|
||||
|
||||
var anilistId: String? = null
|
||||
var kitsuId: String? = null
|
||||
var myAnimeListId: String? = null
|
||||
var mangaUpdatesId: String? = null
|
||||
var animePlanetId: String? = null
|
||||
override var anilistId: String? = null
|
||||
override var kitsuId: String? = null
|
||||
override var myAnimeListId: String? = null
|
||||
override var mangaUpdatesId: String? = null
|
||||
override var animePlanetId: String? = null
|
||||
|
||||
var status: Int? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package exh.metadata.metadata.base
|
||||
|
||||
interface TrackerIdMetadata {
|
||||
var anilistId: String?
|
||||
var kitsuId: String?
|
||||
var myAnimeListId: String?
|
||||
var mangaUpdatesId: String?
|
||||
var animePlanetId: String?
|
||||
}
|
||||
Loading…
Reference in a new issue