fix(media): catch menu client and refresh failures

This commit is contained in:
edde746
2026-06-13 13:25:48 +02:00
parent ebd850b0d5
commit 2876e1bf78
3 changed files with 109 additions and 27 deletions
+1 -21
View File
@@ -2590,15 +2590,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
context,
metadata: episode,
isOffline: widget.isOffline,
onRefresh: () async {
final refreshed = await client?.fetchItem(episode.id);
if (refreshed != null) {
setStateIfMounted(() {
_episodes[index] = refreshed;
_syncEpisodeToCache(index, refreshed);
});
}
},
onRefresh: () => unawaited(_refreshItemInPlace(episode.id)),
);
},
onRefresh: widget.isOffline ? null : _refreshItemInPlace,
@@ -2674,18 +2666,6 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
);
}
/// Sync an updated episode back into the episode cache
void _syncEpisodeToCache(int episodeIndex, MediaItem updated) {
if (_isFlattenEpisodeList) {
_allEpisodes = _allEpisodes.replaceItems(_episodes);
return;
}
if (_seasons.isEmpty) return;
if (_selectedSeasonIndex >= _seasons.length) return;
final season = _seasons[_selectedSeasonIndex];
_seasonEpisodePager.updateEpisode(season.id, episodeIndex, updated);
}
void _syncFlattenEpisodeState() {
if (_isFlattenEpisodeList) _allEpisodes = _allEpisodes.replaceItems(_episodes);
}
+17 -6
View File
@@ -553,7 +553,10 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// Queue for later sync — the offline provider emits the WatchStateEvent.
await WatchActions.setWatched(context, item, watched: watched, offline: true);
if (context.mounted) {
showAppSnackBar(context, watched ? t.messages.markedAsWatchedOffline : t.messages.markedAsUnwatchedOffline);
showAppSnackBar(
context,
watched ? t.messages.markedAsWatchedOffline : t.messages.markedAsUnwatchedOffline,
);
_notifyRefresh(item.id);
}
} else {
@@ -708,6 +711,11 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await _handleDeleteMediaItem(context, mediaKind);
break;
}
} catch (e, st) {
appLogger.e('Media context menu action failed', error: e, stackTrace: st);
if (context.mounted) {
showErrorSnackBar(context, t.messages.errorLoading(error: e.toString()));
}
} finally {
_isContextMenuOpen = false;
@@ -810,11 +818,13 @@ class MediaContextMenuState extends State<MediaContextMenu> {
}
Future<void> _showFileInfo(BuildContext context) async {
final client = _getMediaClientForItem();
var loadingShown = false;
try {
final client = _getMediaClientForItem();
if (context.mounted) {
showLoadingDialog(context);
loadingShown = true;
}
// Fetch file info
@@ -822,8 +832,9 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final fileInfo = await client.getFileInfo(item);
// Close loading indicator
if (context.mounted) {
if (loadingShown && context.mounted) {
Navigator.pop(context);
loadingShown = false;
}
if (fileInfo != null && context.mounted) {
@@ -838,7 +849,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
}
} catch (e) {
// Close loading indicator if it's still open
if (context.mounted && Navigator.canPop(context)) {
if (loadingShown && context.mounted && Navigator.canPop(context)) {
Navigator.pop(context);
}
@@ -1346,10 +1357,10 @@ class MediaContextMenuState extends State<MediaContextMenu> {
Future<void> _handleDownload(BuildContext context) async {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
final item = _mediaItem!;
// Backend-agnostic resolve so Jellyfin items can be downloaded too.
final client = context.getMediaClientWithFallback(serverIdOrNull(_itemServerId));
try {
// Backend-agnostic resolve so Jellyfin items can be downloaded too.
final client = context.getMediaClientWithFallback(serverIdOrNull(_itemServerId));
final result = await showDownloadOptionsAndQueue(
context,
metadata: item,
+91
View File
@@ -1,16 +1,34 @@
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:plezy/connection/connection.dart';
import 'package:plezy/connection/connection_registry.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/metadata_edit/metadata_edit_adapters.dart';
import 'package:plezy/models/plex/plex_home_user.dart';
import 'package:plezy/profiles/profile.dart';
import 'package:plezy/profiles/active_profile_provider.dart';
import 'package:plezy/profiles/plex_home_service.dart';
import 'package:plezy/profiles/profile_connection_registry.dart';
import 'package:plezy/profiles/profile_registry.dart';
import 'package:plezy/providers/multi_server_provider.dart';
import 'package:plezy/services/data_aggregation_service.dart';
import 'package:plezy/services/jellyfin_client.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/theme/mono_theme.dart';
import 'package:plezy/utils/platform_detector.dart';
import 'package:plezy/widgets/media_context_menu.dart';
import 'package:provider/provider.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('isAdminActionAllowedForMediaItem', () {
test('blocks non-admin Plex Home users on Plex items', () {
final profile = Profile.virtualPlexHome(connectionId: 'plex-1', homeUser: _homeUser(admin: false));
@@ -57,6 +75,79 @@ void main() {
expect(supportsMetadataEdit(client, MediaKind.track), isFalse);
});
});
group('MediaContextMenu actions', () {
testWidgets('file info client resolution failure shows an error without popping another route', (tester) async {
LocaleSettings.setLocaleSync(AppLocale.en);
TvDetectionService.debugSetAppleTVOverride(true);
addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null));
final db = AppDatabase.forTesting(NativeDatabase.memory());
final manager = MultiServerManager();
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
final connections = ConnectionRegistry(db);
final profileConnections = ProfileConnectionRegistry(db);
final plexHome = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
plexHomeUserFetcher: (_) async => const [],
);
final activeProfileProvider = ActiveProfileProvider(
registry: ProfileRegistry(db),
plexHome: plexHome,
connections: connections,
);
addTearDown(() async {
activeProfileProvider.dispose();
await plexHome.dispose();
multiServerProvider.dispose();
manager.dispose();
await db.close();
});
final menuKey = GlobalKey<MediaContextMenuState>();
final item = MediaItem(
id: 'movie-1',
backend: MediaBackend.jellyfin,
kind: MediaKind.movie,
title: 'Movie',
serverId: 'missing-server',
);
await tester.pumpWidget(
TranslationProvider(
child: MultiProvider(
providers: [
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfileProvider),
],
child: MaterialApp(
theme: monoTheme(dark: true),
home: Scaffold(
body: Center(
child: MediaContextMenu(
key: menuKey,
item: item,
child: const SizedBox(width: 120, height: 80, child: Text('target')),
),
),
),
),
),
),
);
menuKey.currentState!.showContextMenu(tester.element(find.text('target')));
await tester.pumpAndSettle();
await tester.tap(find.text(t.mediaMenu.fileInfo));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.byType(SnackBar), findsOneWidget);
expect(find.text('target'), findsOneWidget);
});
});
}
PlexHomeUser _homeUser({required bool admin}) {