From 16884a7bca35898d63f9bbacf656cfbd08d0231e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:46:45 +0100 Subject: [PATCH] refactor: eliminate duplicate logic across detail screens and helpers --- lib/models/plex_media_info.dart | 26 +- .../base_media_list_detail_screen.dart | 3 +- lib/screens/collection_detail_screen.dart | 53 +-- .../focusable_detail_screen_mixin.dart | 52 +++ .../playlist/playlist_detail_screen.dart | 399 +++++------------- lib/screens/settings/logs_screen.dart | 5 +- lib/screens/settings/mpv_config_screen.dart | 7 +- lib/screens/settings/settings_screen.dart | 30 +- lib/screens/video_player_screen.dart | 13 +- lib/utils/track_label_builder.dart | 36 +- .../widgets/watch_together_overlay.dart | 3 +- 11 files changed, 218 insertions(+), 409 deletions(-) diff --git a/lib/models/plex_media_info.dart b/lib/models/plex_media_info.dart index 01440817..70907874 100644 --- a/lib/models/plex_media_info.dart +++ b/lib/models/plex_media_info.dart @@ -17,6 +17,25 @@ class PlexMediaInfo { int? getPartId() => partId; } +/// Builds a track label from parts with the standard `' · '` joiner pattern. +/// +/// Shared by both Plex track models and MPV track label utilities. +/// If [title] is non-empty it is added first, then [language], then [extraParts]. +/// Falls back to `'$fallbackPrefix ${index + 1}'` when no parts are available. +String buildTrackLabel({ + String? title, + String? language, + List extraParts = const [], + required int index, + String fallbackPrefix = 'Track', +}) { + final parts = []; + if (title != null && title.isNotEmpty) parts.add(title); + if (language != null && language.isNotEmpty) parts.add(language); + parts.addAll(extraParts); + return parts.isEmpty ? '$fallbackPrefix ${index + 1}' : parts.join(' · '); +} + /// Mixin for building track labels with a consistent pattern mixin TrackLabelBuilder { int get id; @@ -31,12 +50,7 @@ mixin TrackLabelBuilder { if (displayTitle != null && displayTitle!.isNotEmpty) { return displayTitle!; } - final parts = []; - if (language != null && language!.isNotEmpty) { - parts.add(language!); - } - parts.addAll(additionalParts); - return parts.isEmpty ? 'Track ${index ?? id}' : parts.join(' · '); + return buildTrackLabel(language: language, extraParts: additionalParts, index: (index ?? id) - 1); } } diff --git a/lib/screens/base_media_list_detail_screen.dart b/lib/screens/base_media_list_detail_screen.dart index ead93dd9..4ae31e96 100644 --- a/lib/screens/base_media_list_detail_screen.dart +++ b/lib/screens/base_media_list_detail_screen.dart @@ -9,6 +9,7 @@ import '../utils/provider_extensions.dart'; import '../services/play_queue_launcher.dart'; import '../models/plex_playlist.dart'; import '../utils/app_logger.dart'; +import '../utils/snackbar_helper.dart'; import '../mixins/refreshable.dart'; import '../mixins/item_updatable.dart'; import '../i18n/strings.g.dart'; @@ -86,7 +87,7 @@ abstract class BaseMediaListDetailScreen extends State Future _playWithShuffle(bool shuffle) async { if (items.isEmpty) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(emptyMessage))); + showAppSnackBar(context, emptyMessage); } return; } diff --git a/lib/screens/collection_detail_screen.dart b/lib/screens/collection_detail_screen.dart index e3110cf0..deed8ad4 100644 --- a/lib/screens/collection_detail_screen.dart +++ b/lib/screens/collection_detail_screen.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../models/plex_metadata.dart'; -import '../widgets/focusable_media_card.dart'; -import '../widgets/media_grid_delegate.dart'; -import '../utils/grid_size_calculator.dart'; import '../widgets/desktop_app_bar.dart'; -import '../providers/settings_provider.dart'; import '../i18n/strings.g.dart'; import '../utils/dialogs.dart'; import '../utils/app_logger.dart'; @@ -149,50 +144,16 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen( - builder: (context, settingsProvider, child) { - final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); - return SliverPadding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - sliver: SliverLayoutBuilder( - builder: (context, constraints) { - final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); - return SliverGrid.builder( - gridDelegate: MediaGridDelegate.createDelegate( - context: context, - density: settingsProvider.libraryDensity, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - final inFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); - final focusNode = index == 0 ? firstItemFocusNode : getGridItemFocusNode(index); - - return FocusableMediaCard( - key: Key(item.ratingKey), - item: item, - focusNode: focusNode, - onRefresh: updateItem, - collectionId: widget.collection.ratingKey, - onListRefresh: loadItems, - onNavigateUp: inFirstRow ? navigateToAppBar : null, - onBack: handleBackFromContent, - onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus), - ); - }, - ); - }, - ), - ); - }, - ); - } } diff --git a/lib/screens/focusable_detail_screen_mixin.dart b/lib/screens/focusable_detail_screen_mixin.dart index 473290d6..6fe843a0 100644 --- a/lib/screens/focusable_detail_screen_mixin.dart +++ b/lib/screens/focusable_detail_screen_mixin.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; import '../focus/dpad_navigator.dart'; import '../focus/input_mode_tracker.dart'; import '../focus/key_event_utils.dart'; +import '../providers/settings_provider.dart'; +import '../utils/grid_size_calculator.dart'; import '../widgets/app_icon.dart'; +import '../widgets/focusable_media_card.dart'; +import '../widgets/media_grid_delegate.dart'; /// Configuration for app bar buttons class AppBarButtonConfig { @@ -244,4 +249,51 @@ mixin FocusableDetailScreenMixin on State { lastFocusedContentVersion = contentVersion; } } + + /// Build a standard focusable grid sliver for media items. + /// Used by collection and smart playlist detail screens. + Widget buildFocusableGrid({ + required List items, + required void Function(String ratingKey) onRefresh, + String? collectionId, + VoidCallback? onListRefresh, + }) { + return Consumer( + builder: (context, settingsProvider, child) { + final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); + return SliverPadding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + sliver: SliverLayoutBuilder( + builder: (context, constraints) { + final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); + return SliverGrid.builder( + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settingsProvider.libraryDensity, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + final inFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); + final focusNode = index == 0 ? firstItemFocusNode : getGridItemFocusNode(index); + + return FocusableMediaCard( + key: Key(item.ratingKey), + item: item, + focusNode: focusNode, + onRefresh: onRefresh, + collectionId: collectionId, + onListRefresh: onListRefresh, + onNavigateUp: inFirstRow ? navigateToAppBar : null, + onBack: handleBackFromContent, + onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus), + ); + }, + ); + }, + ), + ); + }, + ); + } } diff --git a/lib/screens/playlist/playlist_detail_screen.dart b/lib/screens/playlist/playlist_detail_screen.dart index a5323eb6..3360e23c 100644 --- a/lib/screens/playlist/playlist_detail_screen.dart +++ b/lib/screens/playlist/playlist_detail_screen.dart @@ -1,26 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:provider/provider.dart'; import '../../services/plex_client.dart'; import '../../services/play_queue_launcher.dart'; import '../../models/plex_playlist.dart'; import '../../models/plex_metadata.dart'; import '../../utils/app_logger.dart'; import '../../utils/provider_extensions.dart'; -import '../../widgets/focusable_media_card.dart'; -import '../../widgets/media_grid_delegate.dart'; -import '../../utils/grid_size_calculator.dart'; +import '../../widgets/app_icon.dart'; import '../../widgets/desktop_app_bar.dart'; -import '../../providers/settings_provider.dart'; import '../../focus/dpad_navigator.dart'; import '../../focus/input_mode_tracker.dart'; import '../../focus/key_event_utils.dart'; import 'playlist_item_card.dart'; import '../../i18n/strings.g.dart'; import '../../utils/dialogs.dart'; +import '../../utils/snackbar_helper.dart'; import '../base_media_list_detail_screen.dart'; +import '../focusable_detail_screen_mixin.dart'; /// Screen to display the contents of a playlist class PlaylistDetailScreen extends StatefulWidget { @@ -33,7 +30,7 @@ class PlaylistDetailScreen extends StatefulWidget { } class _PlaylistDetailScreenState extends BaseMediaListDetailScreen - with StandardItemLoader { + with StandardItemLoader, FocusableDetailScreenMixin { @override dynamic get mediaItem => widget.playlist; @@ -46,14 +43,41 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen Symbols.playlist_play_rounded; - // Scroll controller for scrolling to top when app bar is focused - final ScrollController _scrollController = ScrollController(); + @override + bool get hasItems => items.isNotEmpty; - // Focus management + @override + int get appBarButtonCount { + int count = 0; + if (items.isNotEmpty) count += 2; // play + shuffle + if (!widget.playlist.smart) count += 1; // delete + return count; + } + + @override + List getAppBarButtons() { + final buttons = []; + if (items.isNotEmpty) { + buttons.add(AppBarButtonConfig(icon: Symbols.play_arrow_rounded, tooltip: t.discover.play, onPressed: playItems)); + buttons.add( + AppBarButtonConfig(icon: Symbols.shuffle_rounded, tooltip: t.common.shuffle, onPressed: shufflePlayItems), + ); + } + if (!widget.playlist.smart) { + buttons.add( + AppBarButtonConfig( + icon: Symbols.delete_rounded, + tooltip: t.playlists.delete, + onPressed: _deletePlaylist, + color: Colors.red, + ), + ); + } + return buttons; + } + + // Focus management for regular (non-smart) reorderable lists final FocusNode _listFocusNode = FocusNode(debugLabel: 'playlist_list'); - final FocusNode _playButtonFocusNode = FocusNode(debugLabel: 'playlist_play'); - final FocusNode _shuffleButtonFocusNode = FocusNode(debugLabel: 'playlist_shuffle'); - final FocusNode _deleteButtonFocusNode = FocusNode(debugLabel: 'playlist_delete'); // Navigation state for regular (non-smart) playlists int _focusedIndex = 0; @@ -67,40 +91,13 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen _gridItemFocusNodes = {}; - int? _lastFocusedGridIndex; - int _contentVersion = 0; - int _lastFocusedContentVersion = 0; - @override void dispose() { - _scrollController.dispose(); _listFocusNode.dispose(); - _playButtonFocusNode.dispose(); - _shuffleButtonFocusNode.dispose(); - _deleteButtonFocusNode.dispose(); - _firstItemFocusNode.dispose(); - for (final node in _gridItemFocusNodes.values) { - node.dispose(); - } - _gridItemFocusNodes.clear(); + disposeFocusResources(); super.dispose(); } - /// Get or create a focus node for a grid item at the given index - FocusNode _getGridItemFocusNode(int index) { - return _gridItemFocusNodes.putIfAbsent(index, () => FocusNode(debugLabel: 'playlist_grid_item_$index')); - } - @override Future> fetchItems() async { return await client.getPlaylist(widget.playlist.ratingKey); @@ -109,7 +106,7 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen loadItems() async { // Increment content version when loading fresh content - _contentVersion++; + contentVersion++; await super.loadItems(); // Auto-focus after load if in keyboard mode @@ -118,12 +115,12 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen currentOffset + viewportHeight - _estimatedItemHeight) { // Scroll so the item sits ~25% from the top of the viewport final scrollTo = (targetOffset - viewportHeight * 0.25).clamp( - _scrollController.position.minScrollExtent, - _scrollController.position.maxScrollExtent, + scrollController.position.minScrollExtent, + scrollController.position.maxScrollExtent, ); - _scrollController.animateTo(scrollTo, duration: const Duration(milliseconds: 200), curve: Curves.easeOut); + scrollController.animateTo(scrollTo, duration: const Duration(milliseconds: 200), curve: Curves.easeOut); } }); } @@ -384,11 +396,11 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen Navigator.pop(context)); - if (backResult != KeyEventResult.ignored) { - return backResult; - } - - if (event is! KeyDownEvent) return KeyEventResult.ignored; - - if (key.isLeftKey && _appBarFocusedButton > 0) { - setState(() => _appBarFocusedButton--); - _focusAppBarButton(_appBarFocusedButton); - return KeyEventResult.handled; - } - if (key.isRightKey && _appBarFocusedButton < maxButton) { - setState(() => _appBarFocusedButton++); - _focusAppBarButton(_appBarFocusedButton); - return KeyEventResult.handled; - } - if (key.isDownKey) { - // Return focus to list/grid - setState(() => _isAppBarFocused = false); - if (items.isNotEmpty) { - if (widget.playlist.smart) { - _navigateToGrid(); - } else { - _listFocusNode.requestFocus(); - } - } - return KeyEventResult.handled; - } - if (key.isSelectKey) { - _triggerAppBarButton(_appBarFocusedButton); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } - - void _focusAppBarButton(int index) { - switch (index) { - case 0: - _playButtonFocusNode.requestFocus(); - break; - case 1: - _shuffleButtonFocusNode.requestFocus(); - break; - case 2: - _deleteButtonFocusNode.requestFocus(); - break; - } - } - - void _triggerAppBarButton(int index) { - switch (index) { - case 0: - playItems(); - break; - case 1: - shufflePlayItems(); - break; - case 2: - if (!widget.playlist.smart) _deletePlaylist(); - break; - } - } - - /// Navigate focus from app bar down to the grid - void _navigateToGrid() { - if (items.isEmpty) return; - - // Check if we should restore focus to the last focused item - final shouldRestoreFocus = - _lastFocusedGridIndex != null && - _lastFocusedContentVersion == _contentVersion && - _lastFocusedGridIndex! < items.length; - - final targetIndex = shouldRestoreFocus ? _lastFocusedGridIndex! : 0; - - if (targetIndex == 0) { - _firstItemFocusNode.requestFocus(); - } else { - _getGridItemFocusNode(targetIndex).requestFocus(); - } - } - - /// Navigate from grid to app bar - void _navigateToAppBar() { - setState(() { - _isAppBarFocused = true; - _appBarFocusedButton = 0; - }); - _playButtonFocusNode.requestFocus(); - // Scroll to top to show the app bar - _scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut); - } - - /// Handle BACK key from grid/list - navigate to app bar and set flag to prevent PopScope exit - void _handleBackFromContent() { - _backHandledByKeyEvent = true; - _navigateToAppBar(); - } - - /// Handle back navigation for PopScope - bool _handleBackNavigation() { - // If BACK was already handled by a key event, don't pop - if (_backHandledByKeyEvent) { - _backHandledByKeyEvent = false; - return false; - } - - // If in move mode, cancel move instead of navigating - if (_movingIndex != null) { - _cancelMoveMode(); - return false; - } - - if (_isAppBarFocused) { - // Already on app bar, allow exit - return true; - } else { - // Focus app bar first - _navigateToAppBar(); - return false; - } - } - - /// Build focusable app bar actions - List _buildFocusableAppBarActions() { - final colorScheme = Theme.of(context).colorScheme; - final isKeyboardMode = InputModeTracker.isKeyboardMode(context); - - Widget buildFocusableButton({ - required FocusNode focusNode, - required int buttonIndex, - required IconData icon, - required String tooltip, - required VoidCallback onPressed, - Color? color, - }) { - final isFocused = isKeyboardMode && _isAppBarFocused && _appBarFocusedButton == buttonIndex; - return Focus( - focusNode: focusNode, - onKeyEvent: _handleAppBarKeyEvent, - child: Container( - decoration: isFocused - ? BoxDecoration(color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)) - : null, - child: IconButton(icon: AppIcon(icon, fill: 1), tooltip: tooltip, onPressed: onPressed, color: color), - ), - ); - } - - return [ - if (items.isNotEmpty) - buildFocusableButton( - focusNode: _playButtonFocusNode, - buttonIndex: 0, - icon: Symbols.play_arrow_rounded, - tooltip: t.discover.play, - onPressed: playItems, - ), - if (items.isNotEmpty) - buildFocusableButton( - focusNode: _shuffleButtonFocusNode, - buttonIndex: 1, - icon: Symbols.shuffle_rounded, - tooltip: t.common.shuffle, - onPressed: shufflePlayItems, - ), - if (!widget.playlist.smart) - buildFocusableButton( - focusNode: _deleteButtonFocusNode, - buttonIndex: 2, - icon: Symbols.delete_rounded, - tooltip: t.playlists.delete, - onPressed: _deletePlaylist, - color: Colors.red, - ), - ]; - } - /// Cancel move mode if active, returns true if cancelled bool _cancelMoveMode() { if (_movingIndex != null) { @@ -707,6 +533,23 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen( - builder: (context, settingsProvider, child) { - final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity); - return SliverPadding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - sliver: SliverLayoutBuilder( - builder: (context, constraints) { - final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent); - return SliverGrid.builder( - gridDelegate: MediaGridDelegate.createDelegate( - context: context, - density: settingsProvider.libraryDensity, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - final isFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); - final focusNode = index == 0 ? _firstItemFocusNode : _getGridItemFocusNode(index); - - return FocusableMediaCard( - key: Key(item.ratingKey), - item: item, - focusNode: focusNode, - onRefresh: updateItem, - onNavigateUp: isFirstRow ? _navigateToAppBar : null, - onBack: _handleBackFromContent, - onFocusChange: (hasFocus) { - if (hasFocus) { - _lastFocusedGridIndex = index; - _lastFocusedContentVersion = _contentVersion; - } - }, - ); - }, - ); - }, - ), - ); - }, - ); - } - /// Build a reorderable list for regular playlists with focus support Widget _buildReorderableList(bool isKeyboardMode) { return SliverReorderableList( @@ -833,7 +632,7 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen { MemoryLogOutput.clearLogs(); _logs = []; }); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.messages.logsCleared))); + showSuccessSnackBar(context, t.messages.logsCleared); } void _copyAllLogs() { @@ -63,7 +64,7 @@ class _LogsScreenState extends State { } } Clipboard.setData(ClipboardData(text: buffer.toString())); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.messages.logsCopied))); + showSuccessSnackBar(context, t.messages.logsCopied); } Color _getLevelColor(Level level) { diff --git a/lib/screens/settings/mpv_config_screen.dart b/lib/screens/settings/mpv_config_screen.dart index 9250a7f8..9a38e393 100644 --- a/lib/screens/settings/mpv_config_screen.dart +++ b/lib/screens/settings/mpv_config_screen.dart @@ -3,6 +3,7 @@ import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../i18n/strings.g.dart'; import '../../models/mpv_config_models.dart'; +import '../../utils/snackbar_helper.dart'; import '../../services/settings_service.dart'; import '../../widgets/desktop_app_bar.dart'; @@ -202,7 +203,7 @@ class _MpvConfigScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetSaved))); + showSuccessSnackBar(context, t.mpvConfig.presetSaved); } } @@ -216,7 +217,7 @@ class _MpvConfigScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetLoaded))); + showAppSnackBar(context, t.mpvConfig.presetLoaded); } } @@ -233,7 +234,7 @@ class _MpvConfigScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetDeleted))); + showSuccessSnackBar(context, t.mpvConfig.presetDeleted); } } } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index be812d7c..0adbe113 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1297,11 +1297,10 @@ class _SettingsScreenState extends State with FocusableTab { TextButton( onPressed: () async { final navigator = Navigator.of(context); - final messenger = ScaffoldMessenger.of(context); await _settingsService.clearCache(); if (mounted) { navigator.pop(); - messenger.showSnackBar(SnackBar(content: Text(t.settings.clearCacheSuccess))); + showSuccessSnackBar(context, t.settings.clearCacheSuccess); } }, child: Text(t.common.clear), @@ -1324,12 +1323,11 @@ class _SettingsScreenState extends State with FocusableTab { TextButton( onPressed: () async { final navigator = Navigator.of(context); - final messenger = ScaffoldMessenger.of(context); await _settingsService.resetAllSettings(); await _keyboardService?.resetToDefaults(); if (mounted) { navigator.pop(); - messenger.showSnackBar(SnackBar(content: Text(t.settings.resetSettingsSuccess))); + showSuccessSnackBar(context, t.settings.resetSettingsSuccess); // Reload settings _loadSettings(); } @@ -1641,11 +1639,10 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> { actions: [ TextButton( onPressed: () async { - final messenger = ScaffoldMessenger.of(context); await widget.keyboardService.resetToDefaults(); await _loadHotkeys(); if (mounted) { - messenger.showSnackBar(SnackBar(content: Text(t.settings.shortcutsReset))); + showSuccessSnackBar(context, t.settings.shortcutsReset); } }, child: Text(t.common.reset), @@ -1696,20 +1693,14 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> { currentHotKey: currentHotkey, onHotKeyRecorded: (newHotkey) async { final navigator = Navigator.of(context); - final messenger = ScaffoldMessenger.of(context); // Check for conflicts final existingAction = widget.keyboardService.getActionForHotkey(newHotkey); if (existingAction != null && existingAction != action) { navigator.pop(); - messenger.showSnackBar( - SnackBar( - content: Text( - t.settings.shortcutAlreadyAssigned( - action: widget.keyboardService.getActionDisplayName(existingAction), - ), - ), - ), + showErrorSnackBar( + context, + t.settings.shortcutAlreadyAssigned(action: widget.keyboardService.getActionDisplayName(existingAction)), ); return; } @@ -1725,12 +1716,9 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> { navigator.pop(); - messenger.showSnackBar( - SnackBar( - content: Text( - t.settings.shortcutUpdated(action: widget.keyboardService.getActionDisplayName(action)), - ), - ), + showSuccessSnackBar( + context, + t.settings.shortcutUpdated(action: widget.keyboardService.getActionDisplayName(action)), ); } }, diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index f6a7fed8..3236c4fb 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -908,7 +908,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } on PlaybackException catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message))); + showErrorSnackBar(context, e.message); } } catch (e) { if (mounted) { @@ -1323,12 +1323,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin appLogger.e('[Player ERROR] $error'); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(t.messages.failedPlayback(action: 'play', error: error)), - duration: const Duration(seconds: 4), - ), - ); + showErrorSnackBar(context, t.messages.failedPlayback(action: 'play', error: error)); } /// Handle notification when native player switched from ExoPlayer to MPV @@ -1336,9 +1331,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin appLogger.i('Player backend switched from ExoPlayer to MPV (native fallback)'); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(t.messages.switchingToCompatiblePlayer), duration: const Duration(seconds: 2)), - ); + showAppSnackBar(context, t.messages.switchingToCompatiblePlayer); } } diff --git a/lib/utils/track_label_builder.dart b/lib/utils/track_label_builder.dart index a74cb003..9258d3e2 100644 --- a/lib/utils/track_label_builder.dart +++ b/lib/utils/track_label_builder.dart @@ -1,6 +1,10 @@ import 'codec_utils.dart'; +import '../models/plex_media_info.dart' show buildTrackLabel; /// Utility for building track labels for audio and subtitle tracks. +/// +/// Delegates to the shared [buildTrackLabel] function so Plex-model and +/// MPV-player label logic stays consistent. class TrackLabelBuilder { TrackLabelBuilder._(); @@ -14,36 +18,30 @@ class TrackLabelBuilder { int? channelsCount, required int index, }) { - final parts = []; - if (title != null && title.isNotEmpty) { - parts.add(title); - } - if (language != null && language.isNotEmpty) { - parts.add(language.toUpperCase()); - } + final extraParts = []; if (codec != null && codec.isNotEmpty) { - parts.add(CodecUtils.formatAudioCodec(codec)); + extraParts.add(CodecUtils.formatAudioCodec(codec)); } if (channelsCount != null) { - parts.add('${channelsCount}ch'); + extraParts.add('${channelsCount}ch'); } - return parts.isEmpty ? 'Audio Track ${index + 1}' : parts.join(' · '); + return buildTrackLabel( + title: title, + language: language?.toUpperCase(), + extraParts: extraParts, + index: index, + fallbackPrefix: 'Audio Track', + ); } /// Build a label for a subtitle track. /// /// Combines title, language, and codec (with friendly codec names). static String buildSubtitleLabel({String? title, String? language, String? codec, required int index}) { - final parts = []; - if (title != null && title.isNotEmpty) { - parts.add(title); - } - if (language != null && language.isNotEmpty) { - parts.add(language.toUpperCase()); - } + final extraParts = []; if (codec != null && codec.isNotEmpty) { - parts.add(CodecUtils.formatSubtitleCodec(codec)); + extraParts.add(CodecUtils.formatSubtitleCodec(codec)); } - return parts.isEmpty ? 'Track ${index + 1}' : parts.join(' · '); + return buildTrackLabel(title: title, language: language?.toUpperCase(), extraParts: extraParts, index: index); } } diff --git a/lib/watch_together/widgets/watch_together_overlay.dart b/lib/watch_together/widgets/watch_together_overlay.dart index edd6c6b0..53e6f856 100644 --- a/lib/watch_together/widgets/watch_together_overlay.dart +++ b/lib/watch_together/widgets/watch_together_overlay.dart @@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../../i18n/strings.g.dart'; +import '../../utils/snackbar_helper.dart'; import '../models/watch_session.dart'; import '../providers/watch_together_provider.dart'; @@ -255,7 +256,7 @@ class _SessionMenuSheet extends StatelessWidget { void _copySessionCode(BuildContext context, String sessionId) { Clipboard.setData(ClipboardData(text: sessionId)); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.watchTogether.sessionCodeCopied))); + showSuccessSnackBar(context, t.watchTogether.sessionCodeCopied); } void _confirmLeave(BuildContext context) {