fix(livetv): keep the favorites filter narrow while favorites reload

Only the load that commits a favorites set writes the loaded flag, so a
refresh keeps the previous set authoritative. Clearing it up front widened
the guide to the full lineup for the whole round-trip and moved the D-pad
cursor when it collapsed back.
This commit is contained in:
edde746
2026-07-27 17:44:52 +02:00
parent e3cdc2039e
commit 6b87b6551e
2 changed files with 58 additions and 11 deletions
+3 -1
View File
@@ -82,6 +82,9 @@ class _LiveTvScreenState extends State<LiveTvScreen>
int _favoritesLoadGeneration = 0; int _favoritesLoadGeneration = 0;
Future<void>? _favoritesLoadFuture; Future<void>? _favoritesLoadFuture;
final SerialFutureQueue _favoritesMutationQueue = SerialFutureQueue(); final SerialFutureQueue _favoritesMutationQueue = SerialFutureQueue();
/// True while [_favoriteChannels] holds an authoritative set. A refresh keeps the previous set live until the new
/// one commits, so the favorites filter never widens mid-load.
bool _favoritesLoaded = false; bool _favoritesLoaded = false;
bool _favoritesWritable = false; bool _favoritesWritable = false;
@@ -423,7 +426,6 @@ class _LiveTvScreenState extends State<LiveTvScreen>
Future<void> _loadFavorites(MultiServerProvider multiServer) async { Future<void> _loadFavorites(MultiServerProvider multiServer) async {
final loadGeneration = ++_favoritesLoadGeneration; final loadGeneration = ++_favoritesLoadGeneration;
_favoritesLoaded = false;
_favoritesWritable = false; _favoritesWritable = false;
final previousStoreBySource = Map<String, String>.of(_favoriteStoreBySource); final previousStoreBySource = Map<String, String>.of(_favoriteStoreBySource);
final scopeByLiveServer = Map<String, _FavoriteScope>.of(_favoriteScopeByLiveServer); final scopeByLiveServer = Map<String, _FavoriteScope>.of(_favoriteScopeByLiveServer);
+55 -10
View File
@@ -52,6 +52,30 @@ void main() {
expect(_guideChannels(tester), isEmpty); expect(_guideChannels(tester), isEmpty);
}); });
testWidgets('refresh keeps the favorites filter narrow while favorites reload', (tester) async {
final harness = await _pumpLiveTvScreen(tester, channelKeys: const ['channel-a', 'channel-b']);
addTearDown(() async {
await tester.pumpWidget(const SizedBox.shrink());
harness.dispose();
});
final favorite = FavoriteChannel(id: 'channel-a', source: 'server://server-a/provider-a');
harness.liveTv.favorites.complete([favorite]);
await tester.pumpAndSettle();
expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']);
await tester.tap(find.byIcon(Symbols.refresh_rounded));
await tester.pumpAndSettle();
expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']);
harness.liveTv.favorites.complete([favorite]);
await tester.pumpAndSettle();
expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']);
});
testWidgets('favorite read failure preserves raw Guide channels', (tester) async { testWidgets('favorite read failure preserves raw Guide channels', (tester) async {
final harness = await _pumpLiveTvScreen(tester); final harness = await _pumpLiveTvScreen(tester);
addTearDown(() async { addTearDown(() async {
@@ -144,8 +168,8 @@ void main() {
List<LiveTvChannel> _guideChannels(WidgetTester tester) => tester.widget<GuideTab>(find.byType(GuideTab)).channels; List<LiveTvChannel> _guideChannels(WidgetTester tester) => tester.widget<GuideTab>(find.byType(GuideTab)).channels;
Future<_LiveTvHarness> _pumpLiveTvScreen(WidgetTester tester) async { Future<_LiveTvHarness> _pumpLiveTvScreen(WidgetTester tester, {List<String>? channelKeys}) async {
final liveTv = _FakeLiveTvSupport(); final liveTv = _FakeLiveTvSupport(channelKeys: channelKeys);
final client = _FakeMediaServerClient(liveTv); final client = _FakeMediaServerClient(liveTv);
final manager = MultiServerManager()..debugRegisterClientForTesting(client); final manager = MultiServerManager()..debugRegisterClientForTesting(client);
final provider = testMultiServerProvider(manager); final provider = testMultiServerProvider(manager);
@@ -207,11 +231,26 @@ class _FakeMediaServerClient implements MediaServerClient {
} }
class _FakeLiveTvSupport implements LiveTvSupport { class _FakeLiveTvSupport implements LiveTvSupport {
_FakeLiveTvSupport({this.serverId = 'server-a', this.storeKey = 'test-store'}); _FakeLiveTvSupport({this.serverId = 'server-a', this.storeKey = 'test-store', List<String>? channelKeys})
: channelKeys = channelKeys ?? [serverId == 'server-a' ? 'channel-a' : 'channel-$serverId'];
final String serverId; final String serverId;
final String storeKey; final String storeKey;
final Completer<List<FavoriteChannel>> favorites = Completer<List<FavoriteChannel>>(); final List<String> channelKeys;
final List<Completer<List<FavoriteChannel>>> _favoriteRequests = [];
int _servedFavoriteRequests = 0;
Completer<List<FavoriteChannel>> get favorites {
if (_favoriteRequests.length > _servedFavoriteRequests) {
return _favoriteRequests[_servedFavoriteRequests];
}
if (_servedFavoriteRequests > 0 && !_favoriteRequests[_servedFavoriteRequests - 1].isCompleted) {
return _favoriteRequests[_servedFavoriteRequests - 1];
}
final request = Completer<List<FavoriteChannel>>();
_favoriteRequests.add(request);
return request;
}
@override @override
LiveTvDvrSupport? get dvr => null; LiveTvDvrSupport? get dvr => null;
@@ -227,18 +266,24 @@ class _FakeLiveTvSupport implements LiveTvSupport {
@override @override
Future<List<LiveTvChannel>> fetchChannels({String? lineup}) async => [ Future<List<LiveTvChannel>> fetchChannels({String? lineup}) async => [
LiveTvChannel( for (final key in channelKeys)
key: serverId == 'server-a' ? 'channel-a' : 'channel-$serverId', LiveTvChannel(
title: serverId == 'server-a' ? 'Unique Channel A' : 'Unique Channel $serverId', key: key,
serverId: serverId, title: key == 'channel-a' ? 'Unique Channel A' : 'Unique Channel $key',
), serverId: serverId,
),
]; ];
@override @override
Future<List<LiveTvProgram>> fetchSchedule({DateTime? from, DateTime? to}) async => const []; Future<List<LiveTvProgram>> fetchSchedule({DateTime? from, DateTime? to}) async => const [];
@override @override
Future<List<FavoriteChannel>> fetchFavoriteChannels() => favorites.future; Future<List<FavoriteChannel>> fetchFavoriteChannels() {
if (_favoriteRequests.length == _servedFavoriteRequests) {
_favoriteRequests.add(Completer<List<FavoriteChannel>>());
}
return _favoriteRequests[_servedFavoriteRequests++].future;
}
final List<Object> writeFailures = []; final List<Object> writeFailures = [];
final List<List<FavoriteChannel>> writes = []; final List<List<FavoriteChannel>> writes = [];