diff --git a/lib/constants/layout_constants.dart b/lib/constants/layout_constants.dart index 68c62510..3420d2cd 100644 --- a/lib/constants/layout_constants.dart +++ b/lib/constants/layout_constants.dart @@ -1,5 +1,4 @@ /// Layout and sizing constants used throughout the application - /// Screen width breakpoints for responsive design class ScreenBreakpoints { /// Breakpoint for tablet devices (600px) diff --git a/lib/screens/base_media_list_detail_screen.dart b/lib/screens/base_media_list_detail_screen.dart index 57d693c7..7ae55205 100644 --- a/lib/screens/base_media_list_detail_screen.dart +++ b/lib/screens/base_media_list_detail_screen.dart @@ -12,18 +12,9 @@ abstract class BaseMediaListDetailScreen extends State with Refreshable, ItemUpdatable { // State properties - concrete implementations to avoid duplication - List _items = []; - bool _isLoading = false; - String? _errorMessage; - - List get items => _items; - set items(List value) => _items = value; - - bool get isLoading => _isLoading; - set isLoading(bool value) => _isLoading = value; - - String? get errorMessage => _errorMessage; - set errorMessage(String? value) => _errorMessage = value; + List items = []; + bool isLoading = false; + String? errorMessage; @override PlexClient get client => context.clientSafe; diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index 993adc10..e36ac821 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -5,7 +5,6 @@ import '../models/plex_hub.dart'; import '../models/plex_metadata.dart'; import '../models/plex_sort.dart'; import '../providers/settings_provider.dart'; -import '../services/settings_service.dart'; import '../utils/provider_extensions.dart'; import '../utils/app_logger.dart'; import '../utils/grid_cross_axis_extent.dart'; diff --git a/lib/screens/library_tabs/library_browse_tab.dart b/lib/screens/library_tabs/library_browse_tab.dart index 29dca645..c1de6bd8 100644 --- a/lib/screens/library_tabs/library_browse_tab.dart +++ b/lib/screens/library_tabs/library_browse_tab.dart @@ -315,7 +315,9 @@ class _LibraryBrowseTabState extends State return RadioListTile( title: Text(_getGroupingLabel(grouping)), value: grouping, + // ignore: deprecated_member_use groupValue: _selectedGrouping, + // ignore: deprecated_member_use onChanged: (value) async { if (value != null) { setState(() { @@ -325,6 +327,8 @@ class _LibraryBrowseTabState extends State final storage = await StorageService.getInstance(); await storage.saveLibraryGrouping(widget.library.key, value); + if (!mounted) return; + Navigator.pop(context); _loadItems(); } diff --git a/lib/screens/playlist_detail_screen.dart b/lib/screens/playlist_detail_screen.dart index b5001d8e..c761030f 100644 --- a/lib/screens/playlist_detail_screen.dart +++ b/lib/screens/playlist_detail_screen.dart @@ -245,6 +245,8 @@ class _PlaylistDetailScreenState return; } + if (!mounted) return; + // Set play queue in provider final playbackState = context.read(); playbackState.setClient(client); diff --git a/lib/screens/playlists_screen.dart b/lib/screens/playlists_screen.dart deleted file mode 100644 index 26fc1fd8..00000000 --- a/lib/screens/playlists_screen.dart +++ /dev/null @@ -1,322 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../client/plex_client.dart'; -import '../models/plex_playlist.dart'; -import '../providers/settings_provider.dart'; -import '../utils/provider_extensions.dart'; -import '../utils/app_logger.dart'; -import '../utils/grid_size_calculator.dart'; -import '../utils/dialogs.dart'; -import '../widgets/desktop_app_bar.dart'; -import '../mixins/refreshable.dart'; -import '../i18n/strings.g.dart'; -import 'playlist_detail_screen.dart'; - -/// Screen to display all video playlists -class PlaylistsScreen extends StatefulWidget { - const PlaylistsScreen({super.key}); - - @override - State createState() => _PlaylistsScreenState(); -} - -class _PlaylistsScreenState extends State with Refreshable { - PlexClient get client => context.clientSafe; - - List _playlists = []; - bool _isLoading = false; - String? _errorMessage; - bool? _filterSmart; - - @override - void initState() { - super.initState(); - _loadPlaylists(); - } - - Future _loadPlaylists() async { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - - try { - final clientProvider = context.plexClient; - final client = clientProvider.client; - if (client == null) { - throw Exception('No client available'); - } - - final playlists = await client.getPlaylists( - playlistType: 'video', - smart: _filterSmart, - ); - - setState(() { - _playlists = playlists; - _isLoading = false; - }); - - appLogger.d('Loaded ${playlists.length} playlists'); - } catch (e) { - appLogger.e('Failed to load playlists', error: e); - setState(() { - _errorMessage = 'Failed to load playlists: ${e.toString()}'; - _isLoading = false; - }); - } - } - - void _toggleSmartFilter() { - setState(() { - if (_filterSmart == null) { - _filterSmart = true; // Show only smart - } else if (_filterSmart == true) { - _filterSmart = false; // Show only regular - } else { - _filterSmart = null; // Show all - } - }); - _loadPlaylists(); - } - - String _getFilterLabel() { - if (_filterSmart == null) return 'All'; - if (_filterSmart == true) return 'Smart'; - return 'Regular'; - } - - @override - void refresh() { - _loadPlaylists(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - CustomAppBar( - title: Text(t.playlists.title), - pinned: true, - actions: [ - TextButton.icon( - icon: const Icon(Icons.filter_list), - label: Text(_getFilterLabel()), - onPressed: _toggleSmartFilter, - ), - ], - ), - if (_errorMessage != null) - SliverFillRemaining( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - size: 48, - color: Colors.red, - ), - const SizedBox(height: 16), - Text(_errorMessage!), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadPlaylists, - child: Text(t.common.retry), - ), - ], - ), - ), - ) - else if (_playlists.isEmpty && _isLoading) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), - ) - else if (_playlists.isEmpty) - SliverFillRemaining( - child: Center(child: Text(t.playlists.noPlaylists)), - ) - else - SliverPadding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: GridSizeCalculator.getMaxCrossAxisExtent( - context, - context.watch().libraryDensity, - ), - childAspectRatio: 2 / 3.3, - crossAxisSpacing: 0, - mainAxisSpacing: 0, - ), - delegate: SliverChildBuilderDelegate((context, index) { - return _PlaylistCard( - playlist: _playlists[index], - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - PlaylistDetailScreen(playlist: _playlists[index]), - ), - ).then((_) => _loadPlaylists()); // Refresh on return - }, - onDeleted: _loadPlaylists, - ); - }, childCount: _playlists.length), - ), - ), - ], - ), - ); - } -} - -/// Widget to display a single playlist card -class _PlaylistCard extends StatelessWidget { - final PlexPlaylist playlist; - final VoidCallback onTap; - final VoidCallback onDeleted; - - const _PlaylistCard({ - required this.playlist, - required this.onTap, - required this.onDeleted, - }); - - Future _showDeleteDialog(BuildContext context) async { - final confirmed = await showDeleteConfirmation( - context, - title: t.playlists.deleteConfirm, - message: t.playlists.deleteMessage(name: playlist.title), - ); - - if (confirmed == true && context.mounted) { - final client = context.clientSafe; - final success = await client.deletePlaylist(playlist.ratingKey); - - if (context.mounted) { - if (success) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(t.playlists.deleted))); - onDeleted(); - } else { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(t.playlists.errorDeleting))); - } - } - } - } - - @override - Widget build(BuildContext context) { - final client = context.clientSafe; - final imageUrl = playlist.displayImage != null - ? client.getThumbnailUrl(playlist.displayImage!) - : null; - - return Card( - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.all(4), - child: InkWell( - onTap: onTap, - onLongPress: () => _showDeleteDialog(context), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Playlist image - Expanded( - child: Stack( - fit: StackFit.expand, - children: [ - if (imageUrl != null) - Image.network( - imageUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _buildPlaceholder(); - }, - ) - else - _buildPlaceholder(), - // Smart playlist indicator - if (playlist.smart) - Positioned( - top: 4, - right: 4, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.9), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.auto_awesome, - size: 12, - color: Colors.white, - ), - ), - ), - ], - ), - ), - // Playlist info - Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - playlist.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 13, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.playlist_play, - size: 14, - color: Colors.grey[600], - ), - const SizedBox(width: 4), - Text( - playlist.leafCount != null && playlist.leafCount! > 0 - ? (playlist.leafCount == 1 - ? t.playlists.oneItem - : t.playlists.itemCount( - count: playlist.leafCount!, - )) - : t.playlists.emptyPlaylist, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildPlaceholder() { - return Container( - color: Colors.grey[850], - child: const Center( - child: Icon(Icons.playlist_play, size: 48, color: Colors.grey), - ), - ); - } -} diff --git a/lib/utils/collection_playlist_play_helper.dart b/lib/utils/collection_playlist_play_helper.dart index 61a98cf4..6203f34b 100644 --- a/lib/utils/collection_playlist_play_helper.dart +++ b/lib/utils/collection_playlist_play_helper.dart @@ -64,6 +64,8 @@ Future playCollectionOrPlaylist({ playbackState.setClient(client); await playbackState.setPlaybackFromPlayQueue(fetchedQueue, ratingKey); + if (!context.mounted) return; + // Navigate to first item await navigateToVideoPlayer( context, @@ -91,6 +93,8 @@ Future playCollectionOrPlaylist({ playbackState.setClient(client); await playbackState.setPlaybackFromPlayQueue(playQueue, ratingKey); + if (!context.mounted) return; + // Navigate to first item await navigateToVideoPlayer(context, metadata: playQueue.items!.first); } catch (e) { diff --git a/lib/utils/shuffle_play_helper.dart b/lib/utils/shuffle_play_helper.dart index 58abed1c..a14ab81e 100644 --- a/lib/utils/shuffle_play_helper.dart +++ b/lib/utils/shuffle_play_helper.dart @@ -90,6 +90,7 @@ Future handleShufflePlay( episodes.shuffle(); // Store shuffle queue in provider + // ignore: deprecated_member_use_from_same_package playbackState.setShuffleQueue(episodes, metadata.ratingKey); // Navigate to first episode diff --git a/lib/widgets/empty_state_widget.dart b/lib/widgets/empty_state_widget.dart index 72535100..d8f83a8e 100644 --- a/lib/widgets/empty_state_widget.dart +++ b/lib/widgets/empty_state_widget.dart @@ -34,7 +34,9 @@ class EmptyStateWidget extends StatelessWidget { Icon( icon, size: 64, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.4), ), const SizedBox(height: 16), ], @@ -42,7 +44,9 @@ class EmptyStateWidget extends StatelessWidget { message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), ), ), if (onAction != null && actionLabel != null) ...[ diff --git a/lib/widgets/folder_tree_item.dart b/lib/widgets/folder_tree_item.dart index 4c765325..9b5d0c12 100644 --- a/lib/widgets/folder_tree_item.dart +++ b/lib/widgets/folder_tree_item.dart @@ -89,7 +89,9 @@ class FolderTreeItem extends StatelessWidget { size: 20, color: isFolder ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + : Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 12), @@ -115,7 +117,7 @@ class FolderTreeItem extends StatelessWidget { fontSize: 12, color: Theme.of( context, - ).colorScheme.onSurface.withOpacity(0.6), + ).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 94b54cb2..e420a0ce 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -607,13 +607,11 @@ Widget _buildPosterImage(BuildContext context, dynamic item) { IconData fallbackIcon = Icons.movie; if (item is PlexPlaylist) { - posterUrl = (item as PlexPlaylist).displayImage; + posterUrl = item.displayImage; fallbackIcon = Icons.playlist_play; } else if (item is PlexMetadata) { final useSeasonPoster = context.watch().useSeasonPoster; - posterUrl = (item as PlexMetadata).posterThumb( - useSeasonPoster: useSeasonPoster, - ); + posterUrl = item.posterThumb(useSeasonPoster: useSeasonPoster); } if (posterUrl != null) { diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 87a2013a..babdade0 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -608,6 +608,8 @@ class _MediaContextMenuState extends State { final itemUri = await client.buildMetadataUri(metadata.ratingKey); appLogger.d('Built URI for $itemType: $itemUri'); + if (!context.mounted) return; + if (result == '_create_new') { // Create new playlist flow final playlistName = await showDialog( @@ -628,6 +630,8 @@ class _MediaContextMenuState extends State { uri: itemUri, ); + if (!context.mounted) return; + if (context.mounted) { if (newPlaylist != null) { appLogger.d('Successfully created playlist: ${newPlaylist.title}'); @@ -651,6 +655,8 @@ class _MediaContextMenuState extends State { uri: itemUri, ); + if (!context.mounted) return; + if (context.mounted) { if (success) { appLogger.d('Successfully added item(s) to playlist $result'); @@ -819,6 +825,8 @@ class _MediaContextMenuState extends State { type: collectionType, ); + if (!context.mounted) return; + if (context.mounted) { if (newCollectionId != null) { appLogger.d( @@ -834,6 +842,8 @@ class _MediaContextMenuState extends State { uri: itemUri, ); + if (!context.mounted) return; + if (addSuccess) { appLogger.d('Successfully added item to new collection'); ScaffoldMessenger.of( @@ -862,6 +872,8 @@ class _MediaContextMenuState extends State { uri: itemUri, ); + if (!context.mounted) return; + if (context.mounted) { if (success) { appLogger.d('Successfully added item(s) to collection $result');