diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index edae5183..b8076364 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -568,12 +568,13 @@ fun MetaDetailsScreen( val movieProgress = progressByVideoId[meta.id] ?.takeUnless { it.isCompleted } val cwPrefs by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle() - val seriesAction = remember(watchProgressUiState.entries, watchedUiState.items, meta, todayIsoDate, cwPrefs.upNextFromFurthestEpisode) { + val seriesAction = remember(watchProgressUiState.entries, watchedUiState.items, meta, todayIsoDate, cwPrefs.upNextFromFurthestEpisode, watchedUiState.watchedKeys) { meta.seriesPrimaryAction( entries = watchProgressUiState.entries, watchedItems = watchedUiState.items, todayIsoDate = todayIsoDate, preferFurthestEpisode = cwPrefs.upNextFromFurthestEpisode, + watchedKeys = watchedUiState.watchedKeys, ) } val seriesActionVideo = remember(seriesAction, meta.id, meta.videos) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt index 8f548a3c..e6d25f27 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt @@ -2,6 +2,8 @@ package com.nuvio.app.features.details import com.nuvio.app.features.watched.WatchedItem import com.nuvio.app.features.watched.normalizeWatchedMarkedAtEpochMs +import com.nuvio.app.features.watched.watchedItemKey +import com.nuvio.app.features.watching.application.WatchingState import com.nuvio.app.features.watchprogress.WatchProgressEntry import com.nuvio.app.features.watching.domain.WatchingCompletedEpisode import com.nuvio.app.features.watching.domain.WatchingContentRef @@ -144,15 +146,33 @@ internal fun MetaDetails.seriesPrimaryAction( todayIsoDate: String, preferFurthestEpisode: Boolean = true, showUnairedNextUp: Boolean = false, -): SeriesPrimaryAction? = - seriesPrimaryAction( - content = WatchingContentRef(type = type, id = id), + watchedKeys: Set = emptySet(), +): SeriesPrimaryAction? { + val content = WatchingContentRef(type = type, id = id) + val effectiveWatchedItems = buildList { + addAll(watchedItems.filter { it.type.equals(type, ignoreCase = true) && it.id.equals(id, ignoreCase = true) }) + if (watchedKeys.isNotEmpty()) { + val existingKeys = mapTo(mutableSetOf()) { watchedItemKey(it.type, it.id, it.season, it.episode) } + videos.forEach { video -> + val season = video.season ?: return@forEach + val episode = video.episode ?: return@forEach + val key = watchedItemKey(type, id, season, episode) + if (key in existingKeys) return@forEach + if (WatchingState.isEpisodeWatched(watchedKeys, type, id, video)) { + add(WatchedItem(id = id, type = type, season = season, episode = episode, name = "", markedAtEpochMs = 0L)) + } + } + } + } + return seriesPrimaryAction( + content = content, entries = entries, - watchedItems = watchedItems, + watchedItems = effectiveWatchedItems, todayIsoDate = todayIsoDate, preferFurthestEpisode = preferFurthestEpisode, showUnairedNextUp = showUnairedNextUp, ) +} internal fun MetaDetails.seriesPrimaryAction( content: WatchingContentRef, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt index 7de75152..5610f69c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt @@ -100,11 +100,13 @@ object SimklWatchedSyncAdapter : TrackingWatchedProvider { override suspend fun delete(profileId: Int, items: Collection) { if (profileId != ProfileRepository.activeProfileId || items.isEmpty()) return + val episodeItems = items.filter { item -> item.season != null && item.episode != null } + if (episodeItems.isEmpty()) return // Optimistically mark video IDs as removed so fallback won't show them as watched - items.forEach { item -> item.videoId?.let(SimklAnimeWatchedFallback::markOptimisticallyRemoved) } + episodeItems.forEach { item -> item.videoId?.let(SimklAnimeWatchedFallback::markOptimisticallyRemoved) } SimklSyncRepository.ensureLoaded() val snapshot = SimklSyncRepository.state.value.snapshot - val media = items.map { item -> + val media = episodeItems.map { item -> snapshot.mediaReference( contentId = item.id, contentType = item.type, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt index 63126664..d70c98ca 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt @@ -818,6 +818,7 @@ object WatchedRepository { } else if (source.providerId != null) { // Items not found in local store (e.g. anime resolved via snapshot fallback). // Still push delete to provider so it can remove from remote. + // The observer on snapshot changes will re-pull items and update the store. pushDeleteToServer(items = items.toList(), source = source) } } @@ -1010,7 +1011,8 @@ object WatchedRepository { /** * Observes provider extra watched keys (e.g. Simkl anime alternate IDs). * When the provider's snapshot changes (after mutations, syncs), recomputes - * extra keys and re-publishes so watchedKeys stays reactive and current. + * extra keys, re-pulls watched items, and re-publishes so watchedKeys and + * items stay reactive and current. */ private fun startExtraKeysObserverIfNeeded() { if (extraKeysObserverJob != null) return @@ -1021,9 +1023,19 @@ object WatchedRepository { adapter.observeExtraWatchedKeys(currentProfileId) .distinctUntilChanged() .collectLatest { extraKeys -> - val changed = providerExtraWatchedKeys[providerId] != extraKeys - if (changed) { + val keysChanged = providerExtraWatchedKeys[providerId] != extraKeys + if (keysChanged) { providerExtraWatchedKeys[providerId] = extraKeys + // Re-pull items from provider to reflect snapshot changes + // (e.g. after remote episode removal) + val freshItems = adapter.pull( + profileId = currentProfileId, + pageSize = watchedItemsPageSize, + ) + val itemsByKey = freshItems.associateBy { item -> + watchedItemKey(item.type, item.id, item.season, item.episode) + }.toMutableMap() + providerItemsByKey[providerId] = itemsByKey publish() } }