feat(MangaDex): use tracker links to associate mangas automatically with trackers (#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
This commit is contained in:
Tim Schneeberger 2025-03-02 17:37:50 +01:00 committed by GitHub
parent 8d062cecfd
commit 217503eab0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 347 additions and 77 deletions

View File

@ -42,4 +42,11 @@ class TrackPreferences(
"pref_auto_update_manga_on_mark_read", "pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS, AutoTrackState.ALWAYS,
) )
// SY -->
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
"pref_resolve_using_source_metadata_key",
true,
)
// SY <--
} }

View File

@ -59,6 +59,7 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -135,6 +136,13 @@ object SettingsTrackingScreen : SearchableSettings {
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toPersistentMap(), .toPersistentMap(),
), ),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = 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( Preference.PreferenceGroup(
title = stringResource(MR.strings.services), title = stringResource(MR.strings.services),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(

View File

@ -7,6 +7,7 @@ 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.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
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
@ -125,6 +126,12 @@ abstract class BaseTracker(
throw NotImplementedError("Not implemented.") 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 { private suspend fun updateRemote(track: Track): Unit = withIOContext {
try { try {
update(track) update(track)

View File

@ -85,4 +85,8 @@ interface Tracker {
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
// SY -->
suspend fun searchById(id: String): TrackSearch?
// SY <--
} }

View File

@ -237,6 +237,12 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
return api.getMangaMetadata(track) return api.getMangaMetadata(track)
} }
// SY -->
override suspend fun searchById(id: String): TrackSearch {
return api.searchById(id)
}
// SY <--
fun saveOAuth(alOAuth: ALOAuth?) { fun saveOAuth(alOAuth: ALOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth)) trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
} }

View File

@ -5,6 +5,7 @@ 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.ALIdSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata 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
@ -356,6 +357,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 { private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) { if (dateValue == 0L) {
return buildJsonObject { return buildJsonObject {

View File

@ -18,3 +18,16 @@ data class ALSearchPage(
data class ALSearchMedia( data class ALSearchMedia(
val media: List<ALSearchItem>, val media: List<ALSearchItem>,
) )
// SY -->
@Serializable
data class ALIdSearchResult(
val data: ALIdSearchMedia,
)
@Serializable
data class ALIdSearchMedia(
@SerialName("Media")
val media: ALSearchItem,
)
// SY <--

View File

@ -133,6 +133,29 @@ 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 base36Id.toLong(36).let { longId ->
api.getSeries(longId).toTrackSearch(this.id)
}
}
// SY <--
fun restoreSession(): String? { fun restoreSession(): String? {
return trackPreferences.trackPassword(this).get().ifBlank { null } return trackPreferences.trackPassword(this).get().ifBlank { null }
} }

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -25,6 +26,7 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
@ -190,14 +192,35 @@ 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) { return with(json) {
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}")) client.newCall(GET("$BASE_URL/v1/series/$remoteId"))
.awaitSuccess() .awaitSuccess()
.parseAs<MURecord>() .parseAs<MURecord>()
} }
} }
suspend fun convertToNewId(legacyId: Int): String? =
client.newBuilder()
.followRedirects(false)
.build()
.newCall(GET("https://www.mangaupdates.com/series.html?id=$legacyId"))
.await()
.takeIf(Response::isRedirect)
?.header("Location")
?.let {
// Extract the new id from the redirected URL
Regex("""/series/(\w+)(/([\w-]+)?)?/?${'$'}""")
.find(it)
?.groups?.get(1)
?.value
}
// SY <--
companion object { companion object {
private const val BASE_URL = "https://api.mangaupdates.com" private const val BASE_URL = "https://api.mangaupdates.com"

View File

@ -11,7 +11,6 @@ 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
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -161,6 +160,12 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
return api.getMangaMetadata(track) return api.getMangaMetadata(track)
} }
// SY -->
override suspend fun searchById(id: String): TrackSearch {
return api.getMangaDetails(id.toInt())
}
// SY <--
fun getIfAuthExpired(): Boolean { fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this).get() return trackPreferences.trackAuthExpired(this).get()
} }

View File

@ -2,17 +2,20 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -30,6 +33,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign 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.ScreenModel
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
@ -40,6 +45,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.track.TrackChapterSelector import eu.kanade.presentation.track.TrackChapterSelector
import eu.kanade.presentation.track.TrackDateSelector 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.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch 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.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.metadata.metadata.base.TrackerIdMetadata
import exh.source.getMainSource
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -70,6 +79,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.DeleteTrack import tachiyomi.domain.track.interactor.DeleteTrack
@ -102,77 +112,94 @@ data class TrackInfoDialogHomeScreen(
val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()) } val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
TrackInfoDialogHome( // SY -->
trackItems = state.trackItems, Column(modifier = Modifier.animateContentSize()) {
dateFormat = dateFormat, if (state.isLoading) {
onStatusClick = { Column(
navigator.push( modifier = Modifier
TrackStatusSelectorScreen( .fillMaxWidth()
track = it.track!!, .padding(32.dp)
serviceId = it.tracker.id, .windowInsetsPadding(WindowInsets.systemBars),
), verticalArrangement = Arrangement.spacedBy(24.dp),
) horizontalAlignment = Alignment.CenterHorizontally,
}, ) {
onChapterClick = { CircularProgressIndicator()
navigator.push( Text(
TrackChapterSelectorScreen( stringResource(MR.strings.loading),
track = it.track!!, fontSize = 14.sp,
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,
),
) )
} }
}, }
onOpenInBrowser = { openTrackerInBrowser(context, it) }, // SY <--
onRemoved = { else {
navigator.push( TrackInfoDialogHome(
TrackerRemoveScreen( trackItems = state.trackItems,
mangaId = mangaId, dateFormat = dateFormat,
track = it.track!!, onStatusClick = {
serviceId = it.tracker.id, 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) },
) )
}, }
onCopyLink = { context.copyTrackerLink(it) }, }
)
} }
/** /**
@ -196,6 +223,10 @@ data class TrackInfoDialogHomeScreen(
private val mangaId: Long, private val mangaId: Long,
private val sourceId: Long, private val sourceId: Long,
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
/* SY --> */
private val trackerManager: TrackerManager = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
/* SY <-- */
) : StateScreenModel<Model.State>(State()) { ) : StateScreenModel<Model.State>(State()) {
init { init {
@ -225,6 +256,79 @@ 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 sourceManager = Injekt.get<SourceManager>()
val getFlatMetadataById = Injekt.get<GetFlatMetadataById>()
val metadataSource = sourceManager.get(sourceId)
?.getMainSource<MetadataSource<*, *>>() ?: return null
return getFlatMetadataById.await(mangaId)?.run {
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 search manga on tracker by id" }
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() { private suspend fun refreshTrackers() {
val refreshTracks = Injekt.get<RefreshTracks>() val refreshTracks = Injekt.get<RefreshTracks>()
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@ -260,6 +364,9 @@ data class TrackInfoDialogHomeScreen(
@Immutable @Immutable
data class State( data class State(
val trackItems: List<TrackItem> = emptyList(), val trackItems: List<TrackItem> = emptyList(),
// SY -->
val isLoading: Boolean = false,
// SY <--
) )
} }
} }

View File

@ -125,4 +125,6 @@ data class DummyTracker(
): eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata = eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata( ): eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata = eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata(
0, "test", "test", "test", "test", "test", 0, "test", "test", "test", "test", "test",
) )
override suspend fun searchById(id: String) = null
} }

View File

@ -176,6 +176,10 @@
<string name="pref_hide_history_button">Show history in the nav</string> <string name="pref_hide_history_button">Show history in the nav</string>
<string name="pref_show_bottom_bar_labels">Always show nav labels</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 --> <!-- Library settings -->
<string name="pref_sorting_settings">Sorting Settings</string> <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> <string name="pref_skip_pre_migration_summary">Use last saved pre-migration preferences and sources to mass migrate</string>

View File

@ -4,13 +4,14 @@ import android.content.Context
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.copy import eu.kanade.tachiyomi.source.model.copy
import exh.md.utils.MangaDexRelation import exh.md.utils.MangaDexRelation
import exh.metadata.metadata.base.TrackerIdMetadata
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@Serializable @Serializable
class MangaDexSearchMetadata : RaisedSearchMetadata() { class MangaDexSearchMetadata : RaisedSearchMetadata(), TrackerIdMetadata {
var mdUuid: String? = null var mdUuid: String? = null
// var mdUrl: String? = null // var mdUrl: String? = null
@ -31,11 +32,11 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
var rating: Float? = null var rating: Float? = null
// var users: String? = null // var users: String? = null
var anilistId: String? = null override var anilistId: String? = null
var kitsuId: String? = null override var kitsuId: String? = null
var myAnimeListId: String? = null override var myAnimeListId: String? = null
var mangaUpdatesId: String? = null override var mangaUpdatesId: String? = null
var animePlanetId: String? = null override var animePlanetId: String? = null
var status: Int? = null var status: Int? = null

View File

@ -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?
}