fix: preserve live TV state across refreshes
This commit is contained in:
@@ -72,6 +72,9 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
final Map<String, String> _favoriteStoreByChannel = {};
|
final Map<String, String> _favoriteStoreByChannel = {};
|
||||||
final Map<String, String> _favoriteStoreBySource = {};
|
final Map<String, String> _favoriteStoreBySource = {};
|
||||||
final Map<String, FavoriteChannelPersistenceMode> _favoriteModeByStore = {};
|
final Map<String, FavoriteChannelPersistenceMode> _favoriteModeByStore = {};
|
||||||
|
Future<void>? _channelsLoadFuture;
|
||||||
|
int _favoritesLoadGeneration = 0;
|
||||||
|
int _favoritesMutationGeneration = 0;
|
||||||
|
|
||||||
List<LiveTvChannel> get _filteredChannels => filterLiveTvChannelsForFavorites(
|
List<LiveTvChannel> get _filteredChannels => filterLiveTvChannelsForFavorites(
|
||||||
channels: _channels,
|
channels: _channels,
|
||||||
@@ -268,7 +271,18 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
return trimmed == null || trimmed.isEmpty ? null : trimmed;
|
return trimmed == null || trimmed.isEmpty ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadChannels() async {
|
Future<void> _loadChannels() {
|
||||||
|
final inFlight = _channelsLoadFuture;
|
||||||
|
if (inFlight != null) return inFlight;
|
||||||
|
late final Future<void> load;
|
||||||
|
load = _loadChannelsOnce().whenComplete(() {
|
||||||
|
if (identical(_channelsLoadFuture, load)) _channelsLoadFuture = null;
|
||||||
|
});
|
||||||
|
_channelsLoadFuture = load;
|
||||||
|
return load;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadChannelsOnce() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
@@ -289,12 +303,12 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
|
|
||||||
final allChannels = <LiveTvChannel>[];
|
final allChannels = <LiveTvChannel>[];
|
||||||
final seenChannels = <String>{};
|
final seenChannels = <String>{};
|
||||||
_favoriteSourceByLiveServer.clear();
|
final favoriteSourceByLiveServer = <String, String>{};
|
||||||
_favoriteSourceByChannel.clear();
|
final favoriteSourceByChannel = <String, String>{};
|
||||||
_favoriteStoreByLiveServer.clear();
|
final favoriteStoreByLiveServer = <String, String>{};
|
||||||
_favoriteStoreByChannel.clear();
|
final favoriteStoreByChannel = <String, String>{};
|
||||||
_favoriteStoreBySource.clear();
|
final favoriteStoreBySource = <String, String>{};
|
||||||
_favoriteModeByStore.clear();
|
final favoriteModeByStore = <String, FavoriteChannelPersistenceMode>{};
|
||||||
|
|
||||||
appLogger.d(
|
appLogger.d(
|
||||||
'Live TV DVRs: ${liveTvServers.map((s) => '${s.serverId}/${s.dvrKey} lineup=${s.lineup}').join(', ')}',
|
'Live TV DVRs: ${liveTvServers.map((s) => '${s.serverId}/${s.dvrKey} lineup=${s.lineup}').join(', ')}',
|
||||||
@@ -319,10 +333,10 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
final sourceTitle = _sourceTitleForServerInfo(serverInfo);
|
final sourceTitle = _sourceTitleForServerInfo(serverInfo);
|
||||||
final storeKey = liveTv.favoriteStoreKey;
|
final storeKey = liveTv.favoriteStoreKey;
|
||||||
final liveServerKey = _liveServerScopeKey(serverInfo);
|
final liveServerKey = _liveServerScopeKey(serverInfo);
|
||||||
_favoriteSourceByLiveServer[liveServerKey] = source;
|
favoriteSourceByLiveServer[liveServerKey] = source;
|
||||||
_favoriteStoreByLiveServer[liveServerKey] = storeKey;
|
favoriteStoreByLiveServer[liveServerKey] = storeKey;
|
||||||
_favoriteStoreBySource[source] = storeKey;
|
favoriteStoreBySource[source] = storeKey;
|
||||||
_favoriteModeByStore[storeKey] = liveTv.favoritePersistenceMode;
|
favoriteModeByStore[storeKey] = liveTv.favoritePersistenceMode;
|
||||||
|
|
||||||
final channels = await genericClient.liveTv.fetchChannels(lineup: serverInfo.lineup);
|
final channels = await genericClient.liveTv.fetchChannels(lineup: serverInfo.lineup);
|
||||||
// Plex's DVR exposes a separate enabled-channel mapping; Jellyfin
|
// Plex's DVR exposes a separate enabled-channel mapping; Jellyfin
|
||||||
@@ -342,8 +356,8 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
final dedupKey = liveTvChannelScopeKey(scopedChannel);
|
final dedupKey = liveTvChannelScopeKey(scopedChannel);
|
||||||
if (seenChannels.add(dedupKey)) {
|
if (seenChannels.add(dedupKey)) {
|
||||||
final scopeKey = liveTvChannelScopeKey(scopedChannel);
|
final scopeKey = liveTvChannelScopeKey(scopedChannel);
|
||||||
_favoriteSourceByChannel[scopeKey] = source;
|
favoriteSourceByChannel[scopeKey] = source;
|
||||||
_favoriteStoreByChannel[scopeKey] = storeKey;
|
favoriteStoreByChannel[scopeKey] = storeKey;
|
||||||
allChannels.add(scopedChannel);
|
allChannels.add(scopedChannel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,6 +378,24 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_channels = allChannels;
|
_channels = allChannels;
|
||||||
|
_favoriteSourceByLiveServer
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteSourceByLiveServer);
|
||||||
|
_favoriteSourceByChannel
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteSourceByChannel);
|
||||||
|
_favoriteStoreByLiveServer
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteStoreByLiveServer);
|
||||||
|
_favoriteStoreByChannel
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteStoreByChannel);
|
||||||
|
_favoriteStoreBySource
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteStoreBySource);
|
||||||
|
_favoriteModeByStore
|
||||||
|
..clear()
|
||||||
|
..addAll(favoriteModeByStore);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,10 +421,13 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadFavorites(MultiServerProvider multiServer) async {
|
Future<void> _loadFavorites(MultiServerProvider multiServer) async {
|
||||||
|
final loadGeneration = ++_favoritesLoadGeneration;
|
||||||
|
final mutationGeneration = _favoritesMutationGeneration;
|
||||||
try {
|
try {
|
||||||
_favoriteSourceByLiveServer.clear();
|
final sourceByLiveServer = Map<String, String>.of(_favoriteSourceByLiveServer);
|
||||||
_favoriteStoreBySource.clear();
|
final storeByLiveServer = Map<String, String>.of(_favoriteStoreByLiveServer);
|
||||||
_favoriteModeByStore.clear();
|
final storeBySource = Map<String, String>.of(_favoriteStoreBySource);
|
||||||
|
final modeByStore = Map<String, FavoriteChannelPersistenceMode>.of(_favoriteModeByStore);
|
||||||
final merged = <FavoriteChannel>[];
|
final merged = <FavoriteChannel>[];
|
||||||
final fetchedStores = <String>{};
|
final fetchedStores = <String>{};
|
||||||
final seenFavorites = <String>{};
|
final seenFavorites = <String>{};
|
||||||
@@ -403,20 +438,36 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
final source = await liveTv.buildFavoriteChannelSource(lineup: serverInfo.lineup);
|
final source = await liveTv.buildFavoriteChannelSource(lineup: serverInfo.lineup);
|
||||||
final storeKey = liveTv.favoriteStoreKey;
|
final storeKey = liveTv.favoriteStoreKey;
|
||||||
final liveServerKey = _liveServerScopeKey(serverInfo);
|
final liveServerKey = _liveServerScopeKey(serverInfo);
|
||||||
_favoriteSourceByLiveServer[liveServerKey] = source;
|
sourceByLiveServer[liveServerKey] = source;
|
||||||
_favoriteStoreByLiveServer[liveServerKey] = storeKey;
|
storeByLiveServer[liveServerKey] = storeKey;
|
||||||
_favoriteStoreBySource[source] = storeKey;
|
storeBySource[source] = storeKey;
|
||||||
_favoriteModeByStore[storeKey] = liveTv.favoritePersistenceMode;
|
modeByStore[storeKey] = liveTv.favoritePersistenceMode;
|
||||||
if (!fetchedStores.add(storeKey)) continue;
|
if (!fetchedStores.add(storeKey)) continue;
|
||||||
final serverFavorites = await liveTv.fetchFavoriteChannels();
|
final serverFavorites = await liveTv.fetchFavoriteChannels();
|
||||||
for (final favorite in serverFavorites) {
|
for (final favorite in serverFavorites) {
|
||||||
_favoriteStoreBySource[favorite.source] = storeKey;
|
storeBySource[favorite.source] = storeKey;
|
||||||
if (seenFavorites.add(favorite.stableKey)) merged.add(favorite);
|
if (seenFavorites.add(favorite.stableKey)) merged.add(favorite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted ||
|
||||||
|
loadGeneration != _favoritesLoadGeneration ||
|
||||||
|
mutationGeneration != _favoritesMutationGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_favoriteSourceByLiveServer
|
||||||
|
..clear()
|
||||||
|
..addAll(sourceByLiveServer);
|
||||||
|
_favoriteStoreByLiveServer
|
||||||
|
..clear()
|
||||||
|
..addAll(storeByLiveServer);
|
||||||
|
_favoriteStoreBySource
|
||||||
|
..clear()
|
||||||
|
..addAll(storeBySource);
|
||||||
|
_favoriteModeByStore
|
||||||
|
..clear()
|
||||||
|
..addAll(modeByStore);
|
||||||
_favoriteChannels = merged;
|
_favoriteChannels = merged;
|
||||||
_refreshFavoriteKeys();
|
_refreshFavoriteKeys();
|
||||||
});
|
});
|
||||||
@@ -433,6 +484,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _toggleFavorite(LiveTvChannel channel) {
|
void _toggleFavorite(LiveTvChannel channel) {
|
||||||
|
++_favoritesMutationGeneration;
|
||||||
final source = _sourceForChannel(channel);
|
final source = _sourceForChannel(channel);
|
||||||
final favoriteKey = favoriteChannelKey(source, channel.key);
|
final favoriteKey = favoriteChannelKey(source, channel.key);
|
||||||
final scopeKey = liveTvChannelScopeKey(channel);
|
final scopeKey = liveTvChannelScopeKey(channel);
|
||||||
@@ -460,6 +512,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
favorites: List.from(_favoriteChannels),
|
favorites: List.from(_favoriteChannels),
|
||||||
channelMap: channelMap,
|
channelMap: channelMap,
|
||||||
onReorder: (reordered) {
|
onReorder: (reordered) {
|
||||||
|
++_favoritesMutationGeneration;
|
||||||
setState(() {
|
setState(() {
|
||||||
_favoriteChannels = reordered;
|
_favoriteChannels = reordered;
|
||||||
_refreshFavoriteKeys();
|
_refreshFavoriteKeys();
|
||||||
@@ -467,6 +520,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
|||||||
_persistFavorites();
|
_persistFavorites();
|
||||||
},
|
},
|
||||||
onRemove: (removed) {
|
onRemove: (removed) {
|
||||||
|
++_favoritesMutationGeneration;
|
||||||
setState(() {
|
setState(() {
|
||||||
_favoriteChannels = _favoriteChannels.where((f) => f.stableKey != removed.stableKey).toList();
|
_favoriteChannels = _favoriteChannels.where((f) => f.stableKey != removed.stableKey).toList();
|
||||||
_refreshFavoriteKeys();
|
_refreshFavoriteKeys();
|
||||||
|
|||||||
@@ -198,9 +198,9 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin, WidgetsBi
|
|||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
case AppLifecycleState.hidden:
|
case AppLifecycleState.hidden:
|
||||||
_hiddenSince ??= DateTime.now();
|
pauseRefresh();
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
_catchUpIfStale();
|
if (_isGuideVisible) resumeRefresh();
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
break;
|
break;
|
||||||
@@ -1103,6 +1103,7 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin, WidgetsBi
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
_guideFocusNode.requestFocus();
|
_guideFocusNode.requestFocus();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -58,26 +58,45 @@ class _RuleEntry {
|
|||||||
|
|
||||||
enum _RuleAction { edit, delete }
|
enum _RuleAction { edit, delete }
|
||||||
|
|
||||||
class RecordingsTabState extends State<RecordingsTab> {
|
class RecordingsTabState extends State<RecordingsTab> with WidgetsBindingObserver {
|
||||||
List<_ServerRecordings> _serverRecordings = [];
|
List<_ServerRecordings> _serverRecordings = [];
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _adminBlocked = false;
|
bool _adminBlocked = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
bool _pendingFocus = false;
|
bool _pendingFocus = false;
|
||||||
|
bool _refreshRequested = true;
|
||||||
|
bool _tickerEnabled = false;
|
||||||
|
bool _appResumed = true;
|
||||||
final _firstTileFocusNode = FocusNode(debugLabel: 'recordings_tab_first_tile');
|
final _firstTileFocusNode = FocusNode(debugLabel: 'recordings_tab_first_tile');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_load();
|
_load();
|
||||||
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
}
|
||||||
if (mounted) _load();
|
|
||||||
});
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final enabled = TickerMode.valuesOf(context).enabled;
|
||||||
|
if (enabled == _tickerEnabled) return;
|
||||||
|
_tickerEnabled = enabled;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
final resumed = state == AppLifecycleState.resumed;
|
||||||
|
if (resumed == _appResumed) return;
|
||||||
|
_appResumed = resumed;
|
||||||
|
_syncRefreshTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
_firstTileFocusNode.dispose();
|
_firstTileFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -94,14 +113,22 @@ class RecordingsTabState extends State<RecordingsTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void pauseRefresh() => _refreshTimer?.cancel();
|
void pauseRefresh() {
|
||||||
|
_refreshRequested = false;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
void resumeRefresh() {
|
void resumeRefresh() {
|
||||||
|
_refreshRequested = true;
|
||||||
|
_syncRefreshTimer(reload: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncRefreshTimer({bool reload = false}) {
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
_refreshTimer = null;
|
||||||
if (mounted) _load();
|
if (!_refreshRequested || !_tickerEnabled || !_appResumed || !mounted) return;
|
||||||
});
|
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) => _load());
|
||||||
_load();
|
if (reload) unawaited(_load());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public reload helper for the parent screen's refresh action.
|
/// Public reload helper for the parent screen's refresh action.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import '../../../widgets/overlay_sheet.dart';
|
|||||||
import '../../../utils/scroll_utils.dart';
|
import '../../../utils/scroll_utils.dart';
|
||||||
import '../../../widgets/horizontal_scroll_with_arrows.dart';
|
import '../../../widgets/horizontal_scroll_with_arrows.dart';
|
||||||
import '../../../widgets/optimized_media_image.dart';
|
import '../../../widgets/optimized_media_image.dart';
|
||||||
|
import '../../../widgets/sliver_child_memo.dart';
|
||||||
import '../live_tv_actions_mixin.dart';
|
import '../live_tv_actions_mixin.dart';
|
||||||
import '../live_tv_show_schedule_screen.dart';
|
import '../live_tv_show_schedule_screen.dart';
|
||||||
|
|
||||||
@@ -42,11 +43,16 @@ class WhatsOnTab extends StatefulWidget {
|
|||||||
State<WhatsOnTab> createState() => WhatsOnTabState();
|
State<WhatsOnTab> createState() => WhatsOnTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class WhatsOnTabState extends State<WhatsOnTab> with LiveTvActionsMixin<WhatsOnTab>, MountedSetStateMixin {
|
class WhatsOnTabState extends State<WhatsOnTab>
|
||||||
|
with LiveTvActionsMixin<WhatsOnTab>, MountedSetStateMixin, WidgetsBindingObserver {
|
||||||
List<LiveTvHubResult> _hubs = [];
|
List<LiveTvHubResult> _hubs = [];
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
|
final Map<String, GlobalKey<_LiveTvHubSectionState>> _hubKeysById = {};
|
||||||
List<GlobalKey<_LiveTvHubSectionState>> _hubKeys = [];
|
List<GlobalKey<_LiveTvHubSectionState>> _hubKeys = [];
|
||||||
|
bool _refreshRequested = true;
|
||||||
|
bool _tickerEnabled = false;
|
||||||
|
bool _appResumed = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<LiveTvChannel> get liveTvChannels => widget.channels;
|
List<LiveTvChannel> get liveTvChannels => widget.channels;
|
||||||
@@ -54,23 +60,47 @@ class WhatsOnTabState extends State<WhatsOnTab> with LiveTvActionsMixin<WhatsOnT
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_loadHubs();
|
_loadHubs();
|
||||||
_refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) {
|
|
||||||
if (mounted) _loadHubs();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pauseRefresh() => _refreshTimer?.cancel();
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final enabled = TickerMode.valuesOf(context).enabled;
|
||||||
|
if (enabled == _tickerEnabled) return;
|
||||||
|
_tickerEnabled = enabled;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
final resumed = state == AppLifecycleState.resumed;
|
||||||
|
if (resumed == _appResumed) return;
|
||||||
|
_appResumed = resumed;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pauseRefresh() {
|
||||||
|
_refreshRequested = false;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
void resumeRefresh() {
|
void resumeRefresh() {
|
||||||
|
_refreshRequested = true;
|
||||||
|
_syncRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncRefreshTimer() {
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
_refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) {
|
_refreshTimer = null;
|
||||||
if (mounted) _loadHubs();
|
if (!_refreshRequested || !_tickerEnabled || !_appResumed || !mounted) return;
|
||||||
});
|
_refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) => _loadHubs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -83,6 +113,7 @@ class WhatsOnTabState extends State<WhatsOnTab> with LiveTvActionsMixin<WhatsOnT
|
|||||||
final multiServer = context.read<MultiServerProvider>();
|
final multiServer = context.read<MultiServerProvider>();
|
||||||
final liveTvServers = multiServer.liveTvServers;
|
final liveTvServers = multiServer.liveTvServers;
|
||||||
final allHubs = <LiveTvHubResult>[];
|
final allHubs = <LiveTvHubResult>[];
|
||||||
|
final allHubIds = <String>[];
|
||||||
final queriedServers = <String>{};
|
final queriedServers = <String>{};
|
||||||
|
|
||||||
for (final serverInfo in liveTvServers) {
|
for (final serverInfo in liveTvServers) {
|
||||||
@@ -93,16 +124,23 @@ class WhatsOnTabState extends State<WhatsOnTab> with LiveTvActionsMixin<WhatsOnT
|
|||||||
if (client == null) continue;
|
if (client == null) continue;
|
||||||
|
|
||||||
final hubs = await client.getLiveTvHubs();
|
final hubs = await client.getLiveTvHubs();
|
||||||
allHubs.addAll(hubs);
|
for (final hub in hubs) {
|
||||||
|
allHubs.add(hub);
|
||||||
|
allHubIds.add('${serverInfo.serverId}\u0000${hub.hubKey}');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to load hubs from server ${serverInfo.serverId}', error: e);
|
appLogger.e('Failed to load hubs from server ${serverInfo.serverId}', error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final hubIds = allHubIds.toSet();
|
||||||
|
_hubKeysById.removeWhere((id, _) => !hubIds.contains(id));
|
||||||
setState(() {
|
setState(() {
|
||||||
_hubs = allHubs;
|
_hubs = allHubs;
|
||||||
_hubKeys = List.generate(allHubs.length, (_) => GlobalKey<_LiveTvHubSectionState>());
|
_hubKeys = [
|
||||||
|
for (final hubId in allHubIds) _hubKeysById.putIfAbsent(hubId, () => GlobalKey<_LiveTvHubSectionState>()),
|
||||||
|
];
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -224,6 +262,7 @@ class _LiveTvHubSectionState extends State<_LiveTvHubSection> with MountedSetSta
|
|||||||
static const double _leadingPadding = 12.0;
|
static const double _leadingPadding = 12.0;
|
||||||
|
|
||||||
final _selectLongPress = DpadSelectLongPressController();
|
final _selectLongPress = DpadSelectLongPressController();
|
||||||
|
final SliverChildMemo<LiveTvHubEntry> _childMemo = SliverChildMemo<LiveTvHubEntry>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -442,22 +481,27 @@ class _LiveTvHubSectionState extends State<_LiveTvHubSection> with MountedSetSta
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = widget.hub.entries[index];
|
final entry = widget.hub.entries[index];
|
||||||
final isItemFocused = hasFocus && index == _focusedIndex;
|
final isItemFocused = hasFocus && index == _focusedIndex;
|
||||||
|
return _childMemo.widgetFor(
|
||||||
return Padding(
|
index,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
entry,
|
||||||
child: _LiveTvPosterCard(
|
epoch: (cardWidth, posterHeight, widget.hub.entries.length),
|
||||||
entry: entry,
|
salt: isItemFocused,
|
||||||
width: cardWidth,
|
build: () => Padding(
|
||||||
posterHeight: posterHeight,
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
isFocused: isItemFocused,
|
child: _LiveTvPosterCard(
|
||||||
onTap: () {
|
entry: entry,
|
||||||
_onItemTapped(index);
|
width: cardWidth,
|
||||||
widget.onTap(entry);
|
posterHeight: posterHeight,
|
||||||
},
|
isFocused: isItemFocused,
|
||||||
onLongPress: () {
|
onTap: () {
|
||||||
_onItemTapped(index);
|
_onItemTapped(index);
|
||||||
widget.onLongPress(entry);
|
widget.onTap(entry);
|
||||||
},
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
_onItemTapped(index);
|
||||||
|
widget.onLongPress(entry);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user