fix(media): catch menu client and refresh failures
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
Reference in New Issue
Block a user