mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-01 17:16:11 +00:00
Add a setting to let user select anime ids, fix stale sibbilings watched checkmarks and don't use anime ids when providing seasons
This commit is contained in:
parent
c96cda9287
commit
e51d2b047c
11 changed files with 279 additions and 44 deletions
|
|
@ -1144,6 +1144,17 @@
|
|||
<string name="settings_tracking_simkl_progress_description">Resume with playback progress from Simkl.</string>
|
||||
<string name="settings_tracking_tmdb_recommendations_description">Use TMDB recommendations on details pages.</string>
|
||||
<string name="settings_tracking_trakt_recommendations_description">Use personalized related titles from Trakt.</string>
|
||||
<string name="settings_tracking_anime_id_title">Anime ID preference</string>
|
||||
<string name="settings_tracking_anime_id_subtitle">Choose which external ID is used as the primary identifier for anime entries.</string>
|
||||
<string name="settings_tracking_anime_id_dialog_title">Anime ID preference</string>
|
||||
<string name="settings_tracking_anime_id_dialog_subtitle">This controls how anime series are identified. Choosing MAL or Kitsu gives each season its own entry instead of grouping under one IMDB ID.</string>
|
||||
<string name="settings_tracking_anime_id_imdb">Prefer IMDB</string>
|
||||
<string name="settings_tracking_anime_id_imdb_description">Group anime seasons under a shared IMDB ID (default behavior).</string>
|
||||
<string name="settings_tracking_anime_id_mal">Prefer MyAnimeList</string>
|
||||
<string name="settings_tracking_anime_id_mal_description">Each anime season gets its own MAL ID as the primary identifier.</string>
|
||||
<string name="settings_tracking_anime_id_kitsu">Prefer Kitsu</string>
|
||||
<string name="settings_tracking_anime_id_kitsu_description">Each anime season gets its own Kitsu ID as the primary identifier.</string>
|
||||
<string name="settings_tracking_anime_section">Simkl features</string>
|
||||
<string name="settings_simkl_connect">Connect Simkl</string>
|
||||
<string name="settings_simkl_connected_as">Connected as %1$s</string>
|
||||
<string name="settings_simkl_default_user">Simkl user</string>
|
||||
|
|
|
|||
|
|
@ -429,6 +429,7 @@ fun LibraryScreen(
|
|||
LibraryLayoutMode.HORIZONTAL -> librarySections(
|
||||
displaySections = librarySectionsDisplay,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
|
||||
sortOption = effectiveSortOption,
|
||||
onPosterClick = onPosterClick,
|
||||
onSectionViewAllClick = onSectionViewAllClick,
|
||||
|
|
@ -1179,6 +1180,7 @@ private enum class LibraryViewMode {
|
|||
private fun LazyListScope.librarySections(
|
||||
displaySections: List<LibraryDisplaySection>,
|
||||
watchedKeys: Set<String>,
|
||||
fullyWatchedSeriesKeys: Set<String>,
|
||||
sortOption: LibrarySortOption,
|
||||
onPosterClick: ((LibraryItem) -> Unit)?,
|
||||
onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)?,
|
||||
|
|
@ -1214,6 +1216,7 @@ private fun LazyListScope.librarySections(
|
|||
isWatched = WatchingState.isPosterWatched(
|
||||
watchedKeys = watchedKeys,
|
||||
item = posterItem,
|
||||
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
|
||||
),
|
||||
onClick = if (entry.exiting) null else onPosterClick?.let { { it(item) } },
|
||||
onLongClick = if (entry.exiting || entrySource == null) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.nuvio.app.core.ui.NuvioLoadingIndicator
|
|||
import com.nuvio.app.core.ui.nuvio
|
||||
import com.nuvio.app.features.library.LibrarySourceMode
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.simkl.SimklAnimeIdPreference
|
||||
import com.nuvio.app.features.simkl.SimklAuthUiState
|
||||
import com.nuvio.app.features.simkl.SimklConnectionMode
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
|
|
@ -58,6 +59,17 @@ import nuvio.composeapp.generated.resources.settings_tracking_trakt_library_desc
|
|||
import nuvio.composeapp.generated.resources.settings_tracking_trakt_progress_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_trakt_recommendations_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_viewing_discovery
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_dialog_subtitle
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_dialog_title
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_imdb
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_imdb_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_kitsu
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_kitsu_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_mal
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_mal_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_subtitle
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_id_title
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_anime_section
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_comments
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_comments_description
|
||||
import nuvio.composeapp.generated.resources.tracking_source_simkl
|
||||
|
|
@ -122,18 +134,34 @@ internal fun LazyListScope.trackingSettingsContent(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_tracking_viewing_discovery),
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
TrackingViewingAndDiscovery(
|
||||
if (traktUiState.mode == TraktConnectionMode.CONNECTED) {
|
||||
item {
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_tracking_viewing_discovery),
|
||||
isTablet = isTablet,
|
||||
settingsUiState = settingsUiState,
|
||||
traktConnected = traktUiState.mode == TraktConnectionMode.CONNECTED,
|
||||
commentsEnabled = commentsEnabled,
|
||||
onCommentsEnabledChange = onCommentsEnabledChange,
|
||||
)
|
||||
) {
|
||||
TrackingViewingAndDiscovery(
|
||||
isTablet = isTablet,
|
||||
settingsUiState = settingsUiState,
|
||||
traktConnected = true,
|
||||
commentsEnabled = commentsEnabled,
|
||||
onCommentsEnabledChange = onCommentsEnabledChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (simklUiState.mode == SimklConnectionMode.CONNECTED) {
|
||||
item {
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_tracking_anime_section),
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
AnimeIdPreferenceSection(
|
||||
isTablet = isTablet,
|
||||
settingsUiState = settingsUiState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -548,3 +576,59 @@ internal fun effectiveTrackingRecommendationsSource(
|
|||
} else {
|
||||
source
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeIdPreferenceSection(
|
||||
isTablet: Boolean,
|
||||
settingsUiState: TrackingSettingsUiState,
|
||||
) {
|
||||
var showPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
SettingsGroup(isTablet = isTablet) {
|
||||
TrackingPreferenceActionRow(
|
||||
title = stringResource(Res.string.settings_tracking_anime_id_title),
|
||||
description = stringResource(Res.string.settings_tracking_anime_id_subtitle),
|
||||
value = animeIdPreferenceLabel(settingsUiState.simklAnimeIdPreference),
|
||||
isTablet = isTablet,
|
||||
onClick = { showPicker = true },
|
||||
)
|
||||
}
|
||||
|
||||
if (showPicker) {
|
||||
TrackingAdaptivePicker(
|
||||
isTablet = isTablet,
|
||||
title = stringResource(Res.string.settings_tracking_anime_id_dialog_title),
|
||||
subtitle = stringResource(Res.string.settings_tracking_anime_id_dialog_subtitle),
|
||||
selectedValue = settingsUiState.simklAnimeIdPreference,
|
||||
options = animeIdPreferenceOptions(),
|
||||
onSelected = TrackingSettingsRepository::setSimklAnimeIdPreference,
|
||||
onDismiss = { showPicker = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun animeIdPreferenceOptions(): List<TrackingPickerOption<SimklAnimeIdPreference>> = listOf(
|
||||
TrackingPickerOption(
|
||||
value = SimklAnimeIdPreference.IMDB,
|
||||
title = stringResource(Res.string.settings_tracking_anime_id_imdb),
|
||||
description = stringResource(Res.string.settings_tracking_anime_id_imdb_description),
|
||||
),
|
||||
TrackingPickerOption(
|
||||
value = SimklAnimeIdPreference.MAL,
|
||||
title = stringResource(Res.string.settings_tracking_anime_id_mal),
|
||||
description = stringResource(Res.string.settings_tracking_anime_id_mal_description),
|
||||
),
|
||||
TrackingPickerOption(
|
||||
value = SimklAnimeIdPreference.KITSU,
|
||||
title = stringResource(Res.string.settings_tracking_anime_id_kitsu),
|
||||
description = stringResource(Res.string.settings_tracking_anime_id_kitsu_description),
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun animeIdPreferenceLabel(preference: SimklAnimeIdPreference): String = when (preference) {
|
||||
SimklAnimeIdPreference.IMDB -> stringResource(Res.string.settings_tracking_anime_id_imdb)
|
||||
SimklAnimeIdPreference.MAL -> stringResource(Res.string.settings_tracking_anime_id_mal)
|
||||
SimklAnimeIdPreference.KITSU -> stringResource(Res.string.settings_tracking_anime_id_kitsu)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Determines which external ID is used as the canonical content ID for anime entries
|
||||
* resolved through Simkl.
|
||||
*
|
||||
* By default (IMDB), anime entries that share the same IMDB ID get grouped together
|
||||
* (e.g. multiple MAL seasons of one franchise). Choosing MAL or KITSU gives each season
|
||||
* its own canonical ID so they appear as separate items in the library and watched state.
|
||||
*/
|
||||
@Serializable
|
||||
enum class SimklAnimeIdPreference {
|
||||
/** Use IMDB ID when available (groups multiple seasons under one ID). */
|
||||
IMDB,
|
||||
|
||||
/** Prefer MyAnimeList ID — each MAL entry gets its own canonical ID. */
|
||||
MAL,
|
||||
|
||||
/** Prefer Kitsu ID — each Kitsu entry gets its own canonical ID. */
|
||||
KITSU;
|
||||
|
||||
companion object {
|
||||
fun fromStorage(value: String?): SimklAnimeIdPreference =
|
||||
entries.firstOrNull { it.name == value } ?: DEFAULT_SIMKL_ANIME_ID_PREFERENCE
|
||||
}
|
||||
}
|
||||
|
||||
val DEFAULT_SIMKL_ANIME_ID_PREFERENCE: SimklAnimeIdPreference = SimklAnimeIdPreference.IMDB
|
||||
|
|
@ -6,6 +6,7 @@ import com.nuvio.app.features.tracking.TrackingExternalIds
|
|||
import com.nuvio.app.features.tracking.TrackingMediaKind
|
||||
import com.nuvio.app.features.tracking.TrackingMediaReference
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingSettingsRepository
|
||||
import com.nuvio.app.features.tracking.extractTrackingYear
|
||||
import com.nuvio.app.features.tracking.parseTrackingExternalIds
|
||||
import com.nuvio.app.features.tracking.trackingMediaKind
|
||||
|
|
@ -227,32 +228,82 @@ internal fun SimklMedia.toTrackingExternalIds(): TrackingExternalIds = TrackingE
|
|||
kitsu = ids.idValue("kitsu")?.toLongOrNull(),
|
||||
)
|
||||
|
||||
internal fun SimklMedia.canonicalContentId(): String? = when {
|
||||
!ids.idValue("imdb").isNullOrBlank() -> ids.idValue("imdb")
|
||||
!ids.idValue("tmdb").isNullOrBlank() -> "tmdb:${ids.idValue("tmdb")}"
|
||||
!ids.idValue("tvdb").isNullOrBlank() -> "tvdb:${ids.idValue("tvdb")}"
|
||||
!ids.idValue("mal").isNullOrBlank() -> "mal:${ids.idValue("mal")}"
|
||||
!ids.idValue("anidb").isNullOrBlank() -> "anidb:${ids.idValue("anidb")}"
|
||||
!ids.idValue("anilist").isNullOrBlank() -> "anilist:${ids.idValue("anilist")}"
|
||||
!ids.idValue("kitsu").isNullOrBlank() -> "kitsu:${ids.idValue("kitsu")}"
|
||||
!ids.simklIdValue().isNullOrBlank() -> "simkl:${ids.simklIdValue()}"
|
||||
else -> null
|
||||
internal fun SimklMedia.canonicalContentId(): String? =
|
||||
canonicalContentId(TrackingSettingsRepository.uiState.value.simklAnimeIdPreference)
|
||||
|
||||
/**
|
||||
* Resolves the canonical content ID for this media entry.
|
||||
*
|
||||
* For non-anime content the priority is always IMDB → TMDB → TVDB → (fallback chain).
|
||||
* For anime, the [animeIdPreference] allows the user to choose which ID system takes priority:
|
||||
* - [SimklAnimeIdPreference.IMDB]: standard fallback (IMDB wins, seasons get grouped)
|
||||
* - [SimklAnimeIdPreference.MAL]: prefer MAL ID so each season is separate
|
||||
* - [SimklAnimeIdPreference.KITSU]: prefer Kitsu ID so each season is separate
|
||||
*
|
||||
* The function is also available without the preference parameter for call-sites that
|
||||
* don't have anime context (movies/shows always use the default chain).
|
||||
*/
|
||||
internal fun SimklMedia.canonicalContentId(animeIdPreference: SimklAnimeIdPreference): String? {
|
||||
// Try the preferred anime ID first when preference is not IMDB
|
||||
when (animeIdPreference) {
|
||||
SimklAnimeIdPreference.MAL -> {
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { return "mal:$it" }
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { return "kitsu:$it" }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { return "anidb:$it" }
|
||||
}
|
||||
SimklAnimeIdPreference.KITSU -> {
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { return "kitsu:$it" }
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { return "mal:$it" }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { return "anidb:$it" }
|
||||
}
|
||||
SimklAnimeIdPreference.IMDB -> Unit // fall through to standard chain
|
||||
}
|
||||
// Standard fallback chain
|
||||
return when {
|
||||
!ids.idValue("imdb").isNullOrBlank() -> ids.idValue("imdb")
|
||||
!ids.idValue("tmdb").isNullOrBlank() -> "tmdb:${ids.idValue("tmdb")}"
|
||||
!ids.idValue("tvdb").isNullOrBlank() -> "tvdb:${ids.idValue("tvdb")}"
|
||||
!ids.idValue("mal").isNullOrBlank() -> "mal:${ids.idValue("mal")}"
|
||||
!ids.idValue("anidb").isNullOrBlank() -> "anidb:${ids.idValue("anidb")}"
|
||||
!ids.idValue("anilist").isNullOrBlank() -> "anilist:${ids.idValue("anilist")}"
|
||||
!ids.idValue("kitsu").isNullOrBlank() -> "kitsu:${ids.idValue("kitsu")}"
|
||||
!ids.simklIdValue().isNullOrBlank() -> "simkl:${ids.simklIdValue()}"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all content IDs (IMDB, MAL, AniDB, AniList, Kitsu, etc.) that this media entry
|
||||
* is known under. Used to emit watched items under all possible IDs for anime entries so
|
||||
* that the UI can find them regardless of which content ID the addon uses.
|
||||
*
|
||||
* When the user prefers MAL or Kitsu as the canonical anime ID, only the preferred ID type
|
||||
* is emitted to prevent duplicates in continue watching and watched badge resolution.
|
||||
*/
|
||||
private fun SimklMedia.alternateContentIds(): Set<String> = buildSet {
|
||||
ids.idValue("imdb")?.takeIf(String::isNotBlank)?.let(::add)
|
||||
ids.idValue("tmdb")?.takeIf(String::isNotBlank)?.let { add("tmdb:$it") }
|
||||
ids.idValue("tvdb")?.takeIf(String::isNotBlank)?.let { add("tvdb:$it") }
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { add("mal:$it") }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { add("anidb:$it") }
|
||||
ids.idValue("anilist")?.takeIf(String::isNotBlank)?.let { add("anilist:$it") }
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { add("kitsu:$it") }
|
||||
ids.simklIdValue()?.takeIf(String::isNotBlank)?.let { add("simkl:$it") }
|
||||
private fun SimklMedia.alternateContentIds(): Set<String> {
|
||||
val preference = TrackingSettingsRepository.uiState.value.simklAnimeIdPreference
|
||||
return when (preference) {
|
||||
SimklAnimeIdPreference.IMDB -> buildSet {
|
||||
ids.idValue("imdb")?.takeIf(String::isNotBlank)?.let(::add)
|
||||
ids.idValue("tmdb")?.takeIf(String::isNotBlank)?.let { add("tmdb:$it") }
|
||||
ids.idValue("tvdb")?.takeIf(String::isNotBlank)?.let { add("tvdb:$it") }
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { add("mal:$it") }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { add("anidb:$it") }
|
||||
ids.idValue("anilist")?.takeIf(String::isNotBlank)?.let { add("anilist:$it") }
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { add("kitsu:$it") }
|
||||
ids.simklIdValue()?.takeIf(String::isNotBlank)?.let { add("simkl:$it") }
|
||||
}
|
||||
SimklAnimeIdPreference.MAL -> buildSet {
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { add("mal:$it") }
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { add("kitsu:$it") }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { add("anidb:$it") }
|
||||
}
|
||||
SimklAnimeIdPreference.KITSU -> buildSet {
|
||||
ids.idValue("kitsu")?.takeIf(String::isNotBlank)?.let { add("kitsu:$it") }
|
||||
ids.idValue("mal")?.takeIf(String::isNotBlank)?.let { add("mal:$it") }
|
||||
ids.idValue("anidb")?.takeIf(String::isNotBlank)?.let { add("anidb:$it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun simklPosterUrl(path: String?): String? =
|
||||
|
|
@ -400,9 +451,9 @@ private fun daysInMonths(year: Int): IntArray = if (year.isLeapYear()) {
|
|||
*/
|
||||
internal fun TrackingMediaReference.resolveAnimeEpisodeForSimkl(): TrackingMediaReference {
|
||||
if (kind != TrackingMediaKind.ANIME) return this
|
||||
val videoId = catalog?.videoId ?: return this
|
||||
val parsed = parseSimklAnimeVideoId(videoId) ?: return this
|
||||
val videoEpisodeNumber = parsed.episodeNumber ?: return this
|
||||
val videoId = catalog?.videoId ?: return stripAnimeIdsIfSeasoned()
|
||||
val parsed = parseSimklAnimeVideoId(videoId) ?: return stripAnimeIdsIfSeasoned()
|
||||
val videoEpisodeNumber = parsed.episodeNumber ?: return stripAnimeIdsIfSeasoned()
|
||||
|
||||
// Override IDs: use ONLY the anime-specific ID from videoId.
|
||||
// Clear all other IDs to prevent Simkl from matching a wrong entry
|
||||
|
|
@ -428,6 +479,20 @@ internal fun TrackingMediaReference.resolveAnimeEpisodeForSimkl(): TrackingMedia
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If this anime reference uses TVDB-style season coordinates, strip anime-specific IDs
|
||||
* (MAL, Kitsu, AniDB, AniList) to prevent Simkl from matching a wrong per-season entry.
|
||||
* When a season is present the parent IMDB/TMDB is the correct identifier.
|
||||
*/
|
||||
private fun TrackingMediaReference.stripAnimeIdsIfSeasoned(): TrackingMediaReference {
|
||||
val season = episode?.season ?: return this
|
||||
if (season <= 0) return this
|
||||
if (ids.mal == null && ids.kitsu == null && ids.anidb == null && ids.anilist == null && ids.simkl == null) return this
|
||||
return copy(
|
||||
ids = ids.copy(mal = null, kitsu = null, anidb = null, anilist = null, simkl = null),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a contentId to its canonical form by finding a matching Simkl entry.
|
||||
* e.g. "mal:123" → finds entry with mal=123 → returns "tt2560140" (its IMDB/canonical ID)
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ data class SimklSyncUiState(
|
|||
val isLoading: Boolean = false,
|
||||
val hasLoaded: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val projectionVersion: Long = 0L,
|
||||
)
|
||||
|
||||
internal fun Map<String, JsonElement>.idValue(key: String): String? =
|
||||
|
|
|
|||
|
|
@ -229,6 +229,16 @@ object SimklSyncRepository : TrackingProfileStore {
|
|||
SimklSyncStorage.savePayload("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Bumps the projection version to force downstream collectors (CW, library, watched badges)
|
||||
* to recompute their projections without re-fetching from the network.
|
||||
* Call this when a setting that affects projection output changes (e.g. anime ID preference).
|
||||
*/
|
||||
fun invalidateProjections() {
|
||||
val current = _state.value
|
||||
_state.value = current.copy(projectionVersion = current.projectionVersion + 1L)
|
||||
}
|
||||
|
||||
override fun removeStoredProfile(profileId: Int) {
|
||||
SimklSyncStorage.removeProfile(profileId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.features.tracking
|
||||
|
||||
import com.nuvio.app.features.library.LibrarySourceMode
|
||||
import com.nuvio.app.features.simkl.SimklAnimeIdPreference
|
||||
import com.nuvio.app.features.trakt.MoreLikeThisSourcePreference
|
||||
import com.nuvio.app.features.trakt.TraktSettingsRepository
|
||||
import com.nuvio.app.features.trakt.TraktSettingsUiState
|
||||
|
|
@ -36,4 +37,7 @@ object TrackingSettingsRepository {
|
|||
|
||||
fun setMoreLikeThisSource(source: MoreLikeThisSourcePreference) =
|
||||
TraktSettingsRepository.setMoreLikeThisSource(source)
|
||||
|
||||
fun setSimklAnimeIdPreference(preference: SimklAnimeIdPreference) =
|
||||
TraktSettingsRepository.setSimklAnimeIdPreference(preference)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import com.nuvio.app.core.auth.AuthRepository
|
|||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.features.library.LibrarySourceMode
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.simkl.DEFAULT_SIMKL_ANIME_ID_PREFERENCE
|
||||
import com.nuvio.app.features.simkl.SimklAnimeIdPreference
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -55,6 +57,7 @@ data class TraktSettingsUiState(
|
|||
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
||||
val librarySourceMode: LibrarySourceMode = DEFAULT_LIBRARY_SOURCE_MODE,
|
||||
val moreLikeThisSource: MoreLikeThisSourcePreference = DEFAULT_MORE_LIKE_THIS_SOURCE,
|
||||
val simklAnimeIdPreference: SimklAnimeIdPreference = DEFAULT_SIMKL_ANIME_ID_PREFERENCE,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -63,6 +66,7 @@ private data class StoredTraktSettings(
|
|||
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
||||
val librarySourceMode: String? = null,
|
||||
val moreLikeThisSource: String? = null,
|
||||
val simklAnimeIdPreference: String? = null,
|
||||
)
|
||||
|
||||
object TraktSettingsRepository {
|
||||
|
|
@ -131,6 +135,14 @@ object TraktSettingsRepository {
|
|||
persist()
|
||||
}
|
||||
|
||||
fun setSimklAnimeIdPreference(preference: SimklAnimeIdPreference) {
|
||||
ensureLoaded()
|
||||
if (_uiState.value.simklAnimeIdPreference == preference) return
|
||||
_uiState.value = _uiState.value.copy(simklAnimeIdPreference = preference)
|
||||
persist()
|
||||
com.nuvio.app.features.simkl.SimklSyncRepository.invalidateProjections()
|
||||
}
|
||||
|
||||
private fun loadFromDisk() {
|
||||
hasLoaded = true
|
||||
|
||||
|
|
@ -150,6 +162,7 @@ object TraktSettingsRepository {
|
|||
continueWatchingDaysCap = normalizeTraktContinueWatchingDaysCap(stored.continueWatchingDaysCap),
|
||||
librarySourceMode = librarySourceModeFromStorage(stored.librarySourceMode),
|
||||
moreLikeThisSource = MoreLikeThisSourcePreference.fromStorage(stored.moreLikeThisSource),
|
||||
simklAnimeIdPreference = SimklAnimeIdPreference.fromStorage(stored.simklAnimeIdPreference),
|
||||
)
|
||||
} else {
|
||||
TraktSettingsUiState()
|
||||
|
|
@ -164,6 +177,7 @@ object TraktSettingsRepository {
|
|||
continueWatchingDaysCap = state.continueWatchingDaysCap,
|
||||
librarySourceMode = state.librarySourceMode.name,
|
||||
moreLikeThisSource = state.moreLikeThisSource.name,
|
||||
simklAnimeIdPreference = state.simklAnimeIdPreference.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -103,20 +103,25 @@ suspend fun resolveWatchedBadgesBulk(
|
|||
|
||||
fun expandFullyWatchedWithSiblings() {
|
||||
val siblingMap = getActiveProviderSiblingMap()
|
||||
if (siblingMap.isEmpty()) return
|
||||
if (siblingMap.isEmpty()) {
|
||||
WatchedRepository.setExpandedFullyWatchedSeriesKeys(emptySet())
|
||||
return
|
||||
}
|
||||
|
||||
val currentKeys = WatchedRepository.fullyWatchedSeriesKeys.value
|
||||
if (currentKeys.isEmpty()) return
|
||||
// Use base keys (without previous sibling expansion) to avoid feedback loops
|
||||
val baseKeys = WatchedRepository.baseFullyWatchedSeriesKeys()
|
||||
if (baseKeys.isEmpty()) {
|
||||
WatchedRepository.setExpandedFullyWatchedSeriesKeys(emptySet())
|
||||
return
|
||||
}
|
||||
|
||||
val expanded = buildSet {
|
||||
addAll(currentKeys)
|
||||
for (key in currentKeys) {
|
||||
// Extract the contentId from watchedItemKey format ("series:tt1234567" or "tv:tt1234567")
|
||||
// Compute only the sibling-derived keys (not including base keys themselves)
|
||||
val siblingKeys = buildSet {
|
||||
for (key in baseKeys) {
|
||||
val contentId = extractContentIdFromWatchedKey(key) ?: continue
|
||||
val siblings = siblingMap[contentId] ?: continue
|
||||
siblings.forEach { siblingId ->
|
||||
if (siblingId != contentId && !siblingId.startsWith(AMBIGUOUS_MARKER)) {
|
||||
// Build watched key with same type prefix
|
||||
val siblingKey = rebuildWatchedKeyWithSiblingId(key, siblingId)
|
||||
if (siblingKey != null) add(siblingKey)
|
||||
}
|
||||
|
|
@ -124,9 +129,9 @@ fun expandFullyWatchedWithSiblings() {
|
|||
}
|
||||
}
|
||||
|
||||
if (expanded.size > currentKeys.size) {
|
||||
log.i { "Sibling expansion: ${currentKeys.size} -> ${expanded.size} keys" }
|
||||
WatchedRepository.setExpandedFullyWatchedSeriesKeys(expanded)
|
||||
if (siblingKeys != WatchedRepository.currentExpandedSiblingKeys()) {
|
||||
log.i { "Sibling expansion: ${baseKeys.size} base keys -> +${siblingKeys.size} sibling keys" }
|
||||
WatchedRepository.setExpandedFullyWatchedSeriesKeys(siblingKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -933,6 +933,14 @@ object WatchedRepository {
|
|||
persistNuvio()
|
||||
}
|
||||
|
||||
fun currentExpandedSiblingKeys(): Set<String> = expandedSiblingKeys
|
||||
|
||||
/**
|
||||
* Returns the base fully-watched series keys from the active source,
|
||||
* without sibling expansion. Used by sibling expansion to avoid feedback loops.
|
||||
*/
|
||||
fun baseFullyWatchedSeriesKeys(): Set<String> = fullyWatchedSeriesKeysForSource(activeSource)
|
||||
|
||||
private fun pushMarksToServer(
|
||||
items: Collection<WatchedItem>,
|
||||
trackerHistorySync: WatchedTrackerHistorySync,
|
||||
|
|
|
|||
Loading…
Reference in a new issue