refactor(detail): drop hand-patched watch state for store resolution
Delete the per-event list patching, parent-count adjustment, and the screen-local progress overlay; rows and the hero resolve against WatchStateStore at build. Also backfill parentId on playable-descendant pages so hierarchy resolution covers season detail pages.
This commit is contained in:
@@ -53,6 +53,7 @@ import '../utils/grid_size_calculator.dart';
|
||||
import '../utils/layout_constants.dart';
|
||||
import '../providers/download_provider.dart';
|
||||
import '../providers/offline_watch_provider.dart';
|
||||
import '../providers/watch_state_store.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/formatters.dart';
|
||||
@@ -181,22 +182,6 @@ class _SeasonEpisodePager {
|
||||
if (changed) _states[entry.key] = entry.value.replaceItems(next);
|
||||
}
|
||||
}
|
||||
|
||||
void patchWhere(bool Function(String seasonId, MediaItem episode) test, MediaItem Function(MediaItem episode) patch) {
|
||||
for (final entry in _states.entries.toList()) {
|
||||
var changed = false;
|
||||
final next = <MediaItem>[];
|
||||
for (final episode in entry.value.items) {
|
||||
if (test(entry.key, episode)) {
|
||||
changed = true;
|
||||
next.add(patch(episode));
|
||||
} else {
|
||||
next.add(episode);
|
||||
}
|
||||
}
|
||||
if (changed) _states[entry.key] = entry.value.replaceItems(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MediaDetailScreen extends StatefulWidget {
|
||||
@@ -246,7 +231,6 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
bool _showEpisodesDirectly = false;
|
||||
MediaItem? _fullMetadata;
|
||||
MediaItem? _onDeckEpisode;
|
||||
final Map<String, int> _localProgressById = {};
|
||||
bool _isLoadingMetadata = true;
|
||||
List<MediaItem>? _extras;
|
||||
List<MediaHub> _relatedHubs = [];
|
||||
@@ -379,25 +363,14 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
@override
|
||||
void onWatchStateChanged(WatchStateEvent event) {
|
||||
_watchStateChanged = true;
|
||||
final epIndex = _episodes.indexWhere((e) => e.id == event.itemId);
|
||||
if (event.changeType == WatchStateChangeType.removedFromContinueWatching) return;
|
||||
|
||||
if (event.changeType == WatchStateChangeType.removedFromContinueWatching) {
|
||||
return;
|
||||
}
|
||||
// Lists keep their server snapshots untouched; cards and the hero resolve
|
||||
// them against [WatchStateStore] at build, so a rebuild is all the
|
||||
// visuals need.
|
||||
setStateIfMounted(() {});
|
||||
|
||||
if (event.changeType == WatchStateChangeType.progressUpdate) {
|
||||
if (event.viewOffset != null) {
|
||||
_patchLocalProgress(event.itemId, event.viewOffset!, epIndex: epIndex);
|
||||
}
|
||||
if (event.isNowWatched != true) return;
|
||||
}
|
||||
|
||||
_localProgressById.remove(event.itemId);
|
||||
_patchWatchedStateFromEvent(
|
||||
event,
|
||||
epIndex: epIndex,
|
||||
clearWatchedProgress: !widget.isOffline || event.changeType == WatchStateChangeType.progressUpdate,
|
||||
);
|
||||
if (event.changeType == WatchStateChangeType.progressUpdate && event.isNowWatched != true) return;
|
||||
|
||||
if (widget.isOffline) {
|
||||
if (_metadata.isShow) {
|
||||
@@ -408,190 +381,34 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
// Online: refresh server-derived counters and on-deck state. A watched
|
||||
// episode can change the hero play target even when the episode row itself
|
||||
// was already visible and patched locally.
|
||||
// was already visible.
|
||||
unawaited(_refreshWatchState());
|
||||
}
|
||||
|
||||
void _patchWatchedStateFromEvent(WatchStateEvent event, {required int epIndex, required bool clearWatchedProgress}) {
|
||||
final isWatched = event.isNowWatched;
|
||||
if (isWatched == null) return;
|
||||
|
||||
setStateIfMounted(() {
|
||||
final base = _fullMetadata ?? widget.metadata;
|
||||
final episodeDelta = epIndex == -1 ? 0 : _watchStateDelta(_episodes[epIndex], isWatched);
|
||||
|
||||
if (base.id == event.itemId) {
|
||||
_fullMetadata = _withWatchedPatch(base, isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
}
|
||||
|
||||
final onDeckEpisode = _onDeckEpisode;
|
||||
if (onDeckEpisode != null && onDeckEpisode.id == event.itemId) {
|
||||
_onDeckEpisode = _withWatchedPatch(onDeckEpisode, isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
}
|
||||
|
||||
_patchWatchedInListWhere(
|
||||
_seasons,
|
||||
(item) => item.id == event.itemId,
|
||||
isWatched,
|
||||
clearWatchedProgress: clearWatchedProgress,
|
||||
);
|
||||
_patchWatchedInListWhere(
|
||||
_episodes,
|
||||
(item) => item.id == event.itemId,
|
||||
isWatched,
|
||||
clearWatchedProgress: clearWatchedProgress,
|
||||
);
|
||||
_seasonEpisodePager.patchWhere(
|
||||
(_, item) => item.id == event.itemId,
|
||||
(item) => _withWatchedPatch(item, isWatched, clearWatchedProgress: clearWatchedProgress),
|
||||
);
|
||||
final extras = _extras;
|
||||
if (extras != null) {
|
||||
_patchWatchedInListWhere(
|
||||
extras,
|
||||
(item) => item.id == event.itemId,
|
||||
isWatched,
|
||||
clearWatchedProgress: clearWatchedProgress,
|
||||
);
|
||||
}
|
||||
_relatedHubs = _patchWatchedInHubs(
|
||||
_relatedHubs,
|
||||
(item) => item.id == event.itemId,
|
||||
isWatched,
|
||||
clearWatchedProgress: clearWatchedProgress,
|
||||
);
|
||||
|
||||
if (base.id == event.itemId || _seasons.any((season) => season.id == event.itemId)) {
|
||||
_patchLoadedDescendantsOf(event.itemId, isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
} else if (episodeDelta != 0) {
|
||||
_adjustParentWatchCounts(event.parentChain, episodeDelta);
|
||||
}
|
||||
_syncFlattenEpisodeState();
|
||||
});
|
||||
}
|
||||
|
||||
MediaItem _withWatchedPatch(MediaItem item, bool isWatched, {required bool clearWatchedProgress}) {
|
||||
final viewOffsetMs = isWatched && !clearWatchedProgress ? null : 0;
|
||||
var updated = item.copyWith(viewCount: isWatched ? 1 : 0);
|
||||
if (item.leafCount != null || item.viewedLeafCount != null) {
|
||||
updated = updated.copyWith(viewedLeafCount: isWatched ? (item.leafCount ?? item.viewedLeafCount ?? 1) : 0);
|
||||
}
|
||||
return viewOffsetMs == null ? updated : updated.copyWith(viewOffsetMs: viewOffsetMs);
|
||||
}
|
||||
|
||||
int _watchStateDelta(MediaItem item, bool isWatched) {
|
||||
if (item.isWatched == isWatched) return 0;
|
||||
return isWatched ? 1 : -1;
|
||||
}
|
||||
|
||||
void _patchWatchedInListWhere(
|
||||
List<MediaItem> items,
|
||||
bool Function(MediaItem item) test,
|
||||
bool isWatched, {
|
||||
required bool clearWatchedProgress,
|
||||
}) {
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (test(items[i])) {
|
||||
items[i] = _withWatchedPatch(items[i], isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
}
|
||||
/// Session-fresh view of [item]: server snapshot + newest watch-state patch.
|
||||
MediaItem _fresh(MediaItem item) {
|
||||
try {
|
||||
return context.read<WatchStateStore>().apply(item);
|
||||
} on ProviderNotFoundException {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
List<MediaHub> _patchWatchedInHubs(
|
||||
List<MediaHub> hubs,
|
||||
bool Function(MediaItem item) test,
|
||||
bool isWatched, {
|
||||
required bool clearWatchedProgress,
|
||||
}) {
|
||||
var changed = false;
|
||||
final updatedHubs = <MediaHub>[];
|
||||
for (final hub in hubs) {
|
||||
var hubChanged = false;
|
||||
final items = List<MediaItem>.of(hub.items);
|
||||
_patchWatchedInListWhere(
|
||||
items,
|
||||
(item) {
|
||||
final matches = test(item);
|
||||
hubChanged = hubChanged || matches;
|
||||
return matches;
|
||||
},
|
||||
isWatched,
|
||||
clearWatchedProgress: clearWatchedProgress,
|
||||
);
|
||||
changed = changed || hubChanged;
|
||||
updatedHubs.add(hubChanged ? hub.copyWith(items: items) : hub);
|
||||
List<MediaItem> _freshAll(List<MediaItem> items) {
|
||||
try {
|
||||
return context.read<WatchStateStore>().applyAll(items);
|
||||
} on ProviderNotFoundException {
|
||||
return items;
|
||||
}
|
||||
return changed ? updatedHubs : hubs;
|
||||
}
|
||||
|
||||
void _patchLoadedDescendantsOf(String parentId, bool isWatched, {required bool clearWatchedProgress}) {
|
||||
bool isDescendant(MediaItem item) => item.parentChain.contains(parentId);
|
||||
_patchWatchedInListWhere(_seasons, isDescendant, isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
_patchWatchedInListWhere(_episodes, isDescendant, isWatched, clearWatchedProgress: clearWatchedProgress);
|
||||
_seasonEpisodePager.patchWhere(
|
||||
(seasonId, item) => seasonId == parentId || isDescendant(item),
|
||||
(item) => _withWatchedPatch(item, isWatched, clearWatchedProgress: clearWatchedProgress),
|
||||
);
|
||||
}
|
||||
|
||||
void _adjustParentWatchCounts(List<String> parentIds, int delta) {
|
||||
if (parentIds.isEmpty) return;
|
||||
final parentIdSet = parentIds.toSet();
|
||||
final base = _fullMetadata ?? widget.metadata;
|
||||
if (parentIdSet.contains(base.id)) {
|
||||
_fullMetadata = _withAdjustedViewedLeafCount(base, delta);
|
||||
}
|
||||
for (var i = 0; i < _seasons.length; i++) {
|
||||
if (parentIdSet.contains(_seasons[i].id)) {
|
||||
_seasons[i] = _withAdjustedViewedLeafCount(_seasons[i], delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem _withAdjustedViewedLeafCount(MediaItem item, int delta) {
|
||||
final viewedLeafCount = item.viewedLeafCount;
|
||||
if (viewedLeafCount == null) return item;
|
||||
final max = item.leafCount ?? (1 << 30);
|
||||
final next = (viewedLeafCount + delta).clamp(0, max).toInt();
|
||||
return item.copyWith(viewedLeafCount: next);
|
||||
}
|
||||
|
||||
void _patchLocalProgress(String itemId, int viewOffset, {int? epIndex}) {
|
||||
_localProgressById[itemId] = viewOffset;
|
||||
setStateIfMounted(() {
|
||||
final base = _fullMetadata ?? widget.metadata;
|
||||
if (base.id == itemId) {
|
||||
_fullMetadata = base.copyWith(viewOffsetMs: viewOffset);
|
||||
}
|
||||
|
||||
final onDeckEpisode = _onDeckEpisode;
|
||||
if (onDeckEpisode != null && onDeckEpisode.id == itemId) {
|
||||
_onDeckEpisode = onDeckEpisode.copyWith(viewOffsetMs: viewOffset);
|
||||
}
|
||||
|
||||
final index = epIndex ?? _episodes.indexWhere((e) => e.id == itemId);
|
||||
if (index != -1) {
|
||||
final updated = _episodes[index].copyWith(viewOffsetMs: viewOffset);
|
||||
_episodes[index] = updated;
|
||||
_syncEpisodeToCache(index, updated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MediaItem _applyLocalProgress(MediaItem item) {
|
||||
if (!_localProgressById.containsKey(item.id)) return item;
|
||||
return item.copyWith(viewOffsetMs: _localProgressById[item.id]!);
|
||||
}
|
||||
|
||||
MediaItem _normalizeRefreshedItem(MediaItem item, MediaItem fallback) {
|
||||
return _applyLocalProgress(
|
||||
_withFallbackLibrary(
|
||||
item.copyWith(
|
||||
serverId: item.serverId ?? fallback.serverId ?? _metadata.serverId,
|
||||
serverName: item.serverName ?? fallback.serverName ?? _metadata.serverName,
|
||||
),
|
||||
fallback,
|
||||
return _withFallbackLibrary(
|
||||
item.copyWith(
|
||||
serverId: item.serverId ?? fallback.serverId ?? _metadata.serverId,
|
||||
serverName: item.serverName ?? fallback.serverName ?? _metadata.serverName,
|
||||
),
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -781,19 +598,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final metadata = result.item;
|
||||
final onDeckEpisode = result.onDeckEpisode;
|
||||
if (metadata != null) {
|
||||
final refreshedMetadata = _applyLocalProgress(
|
||||
_withFallbackLibrary(
|
||||
metadata.copyWith(serverId: serverId, serverName: serverName ?? metadata.serverName),
|
||||
_metadata,
|
||||
),
|
||||
final refreshedMetadata = _withFallbackLibrary(
|
||||
metadata.copyWith(serverId: serverId, serverName: serverName ?? metadata.serverName),
|
||||
_metadata,
|
||||
);
|
||||
final refreshedOnDeck = onDeckEpisode == null
|
||||
? null
|
||||
: _applyLocalProgress(
|
||||
_withFallbackLibrary(
|
||||
onDeckEpisode.copyWith(serverId: serverId, serverName: serverName ?? onDeckEpisode.serverName),
|
||||
refreshedMetadata,
|
||||
),
|
||||
: _withFallbackLibrary(
|
||||
onDeckEpisode.copyWith(serverId: serverId, serverName: serverName ?? onDeckEpisode.serverName),
|
||||
refreshedMetadata,
|
||||
);
|
||||
setStateIfMounted(() {
|
||||
_fullMetadata = refreshedMetadata;
|
||||
@@ -1425,7 +1238,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
: await context.read<DownloadProvider>().lookupOfflineMetadata(serverId, _metadata.id);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fullMetadata = _applyLocalProgress(cachedMetadata ?? _metadata);
|
||||
_fullMetadata = cachedMetadata ?? _metadata;
|
||||
_isLoadingMetadata = false;
|
||||
_hasLoadedExtras = true;
|
||||
_hasLoadedRelatedHubs = true;
|
||||
@@ -1453,7 +1266,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// Truly orphaned item (server gone) — fall back to widget metadata
|
||||
// and let downstream loaders no-op gracefully.
|
||||
setState(() {
|
||||
_fullMetadata = _applyLocalProgress(_metadata);
|
||||
_fullMetadata = _metadata;
|
||||
_isLoadingMetadata = false;
|
||||
_hasLoadedSeasons = true;
|
||||
_hasLoadedEpisodes = true;
|
||||
@@ -1473,22 +1286,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final serverId = _metadata.serverId;
|
||||
final serverName = _metadata.serverName;
|
||||
final source = metadata ?? _metadata;
|
||||
final base = _applyLocalProgress(
|
||||
_withFallbackLibrary(
|
||||
source.copyWith(serverId: serverId ?? source.serverId, serverName: serverName ?? source.serverName),
|
||||
_metadata,
|
||||
),
|
||||
final base = _withFallbackLibrary(
|
||||
source.copyWith(serverId: serverId ?? source.serverId, serverName: serverName ?? source.serverName),
|
||||
_metadata,
|
||||
);
|
||||
final onDeckWithServerId = onDeckEpisode == null
|
||||
? null
|
||||
: _applyLocalProgress(
|
||||
_withFallbackLibrary(
|
||||
onDeckEpisode.copyWith(
|
||||
serverId: serverId ?? onDeckEpisode.serverId,
|
||||
serverName: serverName ?? onDeckEpisode.serverName,
|
||||
),
|
||||
base,
|
||||
: _withFallbackLibrary(
|
||||
onDeckEpisode.copyWith(
|
||||
serverId: serverId ?? onDeckEpisode.serverId,
|
||||
serverName: serverName ?? onDeckEpisode.serverName,
|
||||
),
|
||||
base,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
@@ -1513,7 +1322,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// Fallback to passed metadata on error
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fullMetadata = _applyLocalProgress(_metadata);
|
||||
_fullMetadata = _metadata;
|
||||
_isLoadingMetadata = false;
|
||||
_hasLoadedExtras = true;
|
||||
_hasLoadedRelatedHubs = true;
|
||||
@@ -1687,8 +1496,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
for (final entry in seasonMap.entries) {
|
||||
final seasonRatingKey = entry.value.first.parentId ?? '';
|
||||
final sortedEpisodes = entry.value..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0));
|
||||
final episodesWithProgress = sortedEpisodes.map(_applyLocalProgress).toList();
|
||||
_seasonEpisodePager.completeFirstPage(seasonRatingKey, episodesWithProgress, episodesWithProgress.length);
|
||||
_seasonEpisodePager.completeFirstPage(seasonRatingKey, sortedEpisodes, sortedEpisodes.length);
|
||||
}
|
||||
|
||||
final onDeckSeasonIndex = _findOnDeckSeasonIndex(seasons);
|
||||
@@ -1718,8 +1526,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0));
|
||||
|
||||
setState(() {
|
||||
final episodes = seasonEpisodes.map(_applyLocalProgress).toList();
|
||||
_allEpisodes = _allEpisodes.completeInitialLoad(episodes, episodes.length);
|
||||
_allEpisodes = _allEpisodes.completeInitialLoad(seasonEpisodes, seasonEpisodes.length);
|
||||
_episodes = _allEpisodes.items;
|
||||
_hasLoadedEpisodes = true;
|
||||
});
|
||||
@@ -1755,8 +1562,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// demand) and clear any prior per-season loading/error state.
|
||||
final cached = _seasonEpisodePager.stateFor(seasonId);
|
||||
if (_seasonEpisodePager.hasState(seasonId) && !cached.isInitialLoading && !cached.initialLoadFailed) {
|
||||
final episodes = cached.items.map(_applyLocalProgress).toList();
|
||||
_seasonEpisodePager.completeFirstPage(seasonId, episodes, cached.totalCount);
|
||||
_seasonEpisodePager.completeFirstPage(seasonId, cached.items, cached.totalCount);
|
||||
setStateIfMounted(() {
|
||||
if (_isSelectedSeason(seasonIndex, seasonId)) {
|
||||
_episodes = List.of(_seasonEpisodePager.stateFor(seasonId).items);
|
||||
@@ -1789,12 +1595,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final allEpisodes = downloadProvider.getDownloadedEpisodesForShow(_metadata.id);
|
||||
final seasonEpisodes = allEpisodes.where((ep) => ep.parentIndex == season.index).toList()
|
||||
..sort((a, b) => (a.index ?? 0).compareTo(b.index ?? 0));
|
||||
final episodes = seasonEpisodes.map(_applyLocalProgress).toList();
|
||||
_completeSeasonEpisodesLoad(
|
||||
seasonIndex: seasonIndex,
|
||||
seasonId: seasonId,
|
||||
episodes: episodes,
|
||||
total: episodes.length,
|
||||
episodes: seasonEpisodes,
|
||||
total: seasonEpisodes.length,
|
||||
generation: generation,
|
||||
);
|
||||
} else {
|
||||
@@ -1822,7 +1627,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
_completeSeasonEpisodesLoad(
|
||||
seasonIndex: seasonIndex,
|
||||
seasonId: seasonId,
|
||||
episodes: page.items.map(_applyLocalProgress).toList(),
|
||||
episodes: page.items,
|
||||
total: page.totalCount,
|
||||
generation: generation,
|
||||
);
|
||||
@@ -1870,11 +1675,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
if (!mounted || _showEpisodesDirectly || seasonIndex >= _seasons.length || _seasons[seasonIndex].id != seasonId) {
|
||||
return;
|
||||
}
|
||||
final episodes = page.items.map(_applyLocalProgress).toList();
|
||||
setStateIfMounted(() {
|
||||
final current = _seasonEpisodePager.stateFor(seasonId);
|
||||
if (_seasonEpisodePager.hasState(seasonId) && !(current.isInitialLoading && !current.hasItems)) return;
|
||||
_seasonEpisodePager.completeFirstPage(seasonId, episodes, page.totalCount);
|
||||
_seasonEpisodePager.completeFirstPage(seasonId, page.items, page.totalCount);
|
||||
if (_isSelectedSeason(seasonIndex, seasonId)) {
|
||||
_episodes = List.of(_seasonEpisodePager.stateFor(seasonId).items);
|
||||
}
|
||||
@@ -1929,9 +1733,13 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
size: _episodesPageSize,
|
||||
);
|
||||
if (!mounted || generation != _episodesLoadGeneration) return;
|
||||
final more = page.items.map(_applyLocalProgress).toList();
|
||||
setStateIfMounted(() {
|
||||
_seasonEpisodePager.completeMoreLoad(seasonId, expectedOffset: loaded, episodes: more, total: page.totalCount);
|
||||
_seasonEpisodePager.completeMoreLoad(
|
||||
seasonId,
|
||||
expectedOffset: loaded,
|
||||
episodes: page.items,
|
||||
total: page.totalCount,
|
||||
);
|
||||
if (_isSelectedSeason(seasonIndex, seasonId)) _episodes = List.of(_seasonEpisodePager.stateFor(seasonId).items);
|
||||
});
|
||||
} catch (e, st) {
|
||||
@@ -2690,9 +2498,8 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final refreshed = await client?.fetchItem(episode.id);
|
||||
if (refreshed != null) {
|
||||
setStateIfMounted(() {
|
||||
final updated = _applyLocalProgress(refreshed);
|
||||
_episodes[index] = updated;
|
||||
_syncEpisodeToCache(index, updated);
|
||||
_episodes[index] = refreshed;
|
||||
_syncEpisodeToCache(index, refreshed);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -2818,9 +2625,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
}
|
||||
|
||||
List<MediaItem> _enrichPlayableEpisodes(List<MediaItem> episodes, ServerId serverId) {
|
||||
// Enrich each episode with serverId/serverName/grandparent fields —
|
||||
// Jellyfin's recursive query doesn't always populate them, and the copy is
|
||||
// a no-op for Plex where the mapper already does.
|
||||
// Enrich each episode with serverId/serverName/parent fields — backends
|
||||
// don't always populate them on recursive queries, and hierarchy-aware
|
||||
// watch-state resolution needs the parentChain intact. The copy is a no-op
|
||||
// where the mapper already did.
|
||||
final fallbackParentId = _metadata.isSeason ? _metadata.id : null;
|
||||
final fallbackGrandparentId = _metadata.isSeason ? (_metadata.grandparentId ?? _metadata.parentId) : _metadata.id;
|
||||
final fallbackGrandparentTitle = _metadata.isSeason
|
||||
? (_metadata.grandparentTitle ?? _metadata.parentTitle)
|
||||
@@ -2831,13 +2640,13 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
e.copyWith(
|
||||
serverId: serverId,
|
||||
serverName: _metadata.serverName ?? e.serverName,
|
||||
parentId: e.parentId ?? fallbackParentId,
|
||||
grandparentId: e.grandparentId ?? fallbackGrandparentId,
|
||||
grandparentTitle: e.grandparentTitle ?? fallbackGrandparentTitle,
|
||||
),
|
||||
_metadata,
|
||||
),
|
||||
)
|
||||
.map(_applyLocalProgress)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -2925,7 +2734,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
final nextEpisode = await offlineWatchProvider.getNextUnwatchedEpisode(_metadata.id);
|
||||
|
||||
setStateIfMounted(() {
|
||||
_onDeckEpisode = nextEpisode == null ? null : _applyLocalProgress(nextEpisode);
|
||||
_onDeckEpisode = nextEpisode;
|
||||
});
|
||||
|
||||
if (nextEpisode != null) {
|
||||
@@ -2941,10 +2750,10 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
/// loaded episode is watched (keep the default S1E1 for a rewatch).
|
||||
void _ensureFallbackOnDeckEpisode() {
|
||||
if (_onDeckEpisode != null) return;
|
||||
final next = firstUnwatchedEpisode(_episodes);
|
||||
final next = firstUnwatchedEpisode(_freshAll(_episodes));
|
||||
if (next == null) return;
|
||||
setStateIfMounted(() {
|
||||
_onDeckEpisode = _applyLocalProgress(next);
|
||||
_onDeckEpisode = next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3043,8 +2852,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use full metadata if loaded, otherwise use passed metadata
|
||||
final metadata = _fullMetadata ?? _metadata;
|
||||
// Session-fresh hero: server snapshot resolved against the watch-state
|
||||
// store (onWatchStateChanged rebuilds on relevant events).
|
||||
final metadata = _fresh(_fullMetadata ?? _metadata);
|
||||
final isShow = metadata.isShow;
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final isTv = PlatformDetector.isTV();
|
||||
@@ -3907,8 +3717,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
}
|
||||
|
||||
if (hasLoadedState) {
|
||||
final episodes = state.items.map(_applyLocalProgress).toList();
|
||||
_seasonEpisodePager.completeFirstPage(season.id, episodes, state.totalCount);
|
||||
_seasonEpisodePager.completeFirstPage(season.id, state.items, state.totalCount);
|
||||
setStateIfMounted(() {
|
||||
_selectedSeasonIndex = seasonIndex;
|
||||
_episodes = List.of(_seasonEpisodePager.stateFor(season.id).items);
|
||||
@@ -4383,7 +4192,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// For TV shows
|
||||
if (metadata.isShow) {
|
||||
if (_onDeckEpisode != null) {
|
||||
final episode = _onDeckEpisode!;
|
||||
final episode = _fresh(_onDeckEpisode!);
|
||||
// Check if episode has been partially watched
|
||||
if (episode.viewOffsetMs != null && episode.viewOffsetMs! > 0) {
|
||||
return Symbols.resume_rounded; // Resume icon
|
||||
|
||||
Reference in New Issue
Block a user