diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index 8088fbf5..4d896367 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -10,6 +10,7 @@ import '../utils/app_logger.dart'; import '../utils/provider_extensions.dart'; import '../utils/video_player_navigation.dart'; import '../utils/content_rating_formatter.dart'; +import '../utils/shuffle_play_helper.dart'; import '../theme/theme_helper.dart'; import 'season_detail_screen.dart'; @@ -574,6 +575,23 @@ class _MediaDetailScreenState extends State { ), ), const SizedBox(width: 12), + // Shuffle button (only for shows and seasons) + if (metadata.type.toLowerCase() == 'show' || + metadata.type.toLowerCase() == 'season') ...[ + IconButton.filledTonal( + onPressed: () async { + await handleShufflePlay(context, metadata); + }, + icon: const Icon(Icons.shuffle), + tooltip: 'Shuffle play', + iconSize: 20, + style: IconButton.styleFrom( + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + ), + ), + const SizedBox(width: 12), + ], IconButton.filledTonal( onPressed: () async { try { diff --git a/lib/utils/shuffle_play_helper.dart b/lib/utils/shuffle_play_helper.dart new file mode 100644 index 00000000..fca5eb04 --- /dev/null +++ b/lib/utils/shuffle_play_helper.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/plex_metadata.dart'; +import '../providers/playback_state_provider.dart'; +import '../utils/provider_extensions.dart'; +import '../utils/video_player_navigation.dart'; + +/// Handle shuffle play action for shows and seasons +/// +/// Fetches all unwatched episodes, shuffles them, and starts playback +/// from the first shuffled episode. The shuffle queue is stored in the +/// PlaybackStateProvider for continuous shuffle playback. +Future handleShufflePlay( + BuildContext context, + PlexMetadata metadata, +) async { + final client = context.client; + if (client == null) return; + + final playbackState = context.read(); + final itemType = metadata.type.toLowerCase(); + + try { + // Show loading indicator + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => + const Center(child: CircularProgressIndicator()), + ); + } + + // Get unwatched episodes based on type + List episodes; + if (itemType == 'show') { + episodes = await client.getAllUnwatchedEpisodes( + metadata.ratingKey, + ); + } else { + // season + episodes = await client.getUnwatchedEpisodesInSeason( + metadata.ratingKey, + ); + } + + // Close loading indicator + if (context.mounted) { + Navigator.pop(context); + } + + if (episodes.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No unwatched episodes found')), + ); + } + return; + } + + // Shuffle the episodes + episodes.shuffle(); + + // Store shuffle queue in provider + playbackState.setShuffleQueue(episodes, metadata.ratingKey); + + // Navigate to first episode + if (context.mounted) { + await navigateToVideoPlayer(context, metadata: episodes.first); + } + } catch (e) { + // Close loading indicator if it's still open + if (context.mounted && Navigator.canPop(context)) { + Navigator.pop(context); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error starting shuffle play: $e')), + ); + } + } +} diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index be94a4f7..95e192b5 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -1,13 +1,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../models/plex_metadata.dart'; import '../utils/provider_extensions.dart'; import '../screens/media_detail_screen.dart'; import '../screens/season_detail_screen.dart'; import '../widgets/file_info_bottom_sheet.dart'; -import '../providers/playback_state_provider.dart'; -import '../utils/video_player_navigation.dart'; +import '../utils/shuffle_play_helper.dart'; /// Helper class to store menu action data class _MenuAction { @@ -251,7 +249,7 @@ class _MediaContextMenuState extends State { break; case 'shuffle_play': - await _handleShufflePlay(context); + await handleShufflePlay(context, widget.metadata); break; } } @@ -364,75 +362,6 @@ class _MediaContextMenuState extends State { } } - /// Handle shuffle play action - Future _handleShufflePlay(BuildContext context) async { - final client = context.client; - if (client == null) return; - - final playbackState = context.read(); - final itemType = widget.metadata.type.toLowerCase(); - - try { - // Show loading indicator - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => - const Center(child: CircularProgressIndicator()), - ); - } - - // Get unwatched episodes based on type - List episodes; - if (itemType == 'show') { - episodes = await client.getAllUnwatchedEpisodes( - widget.metadata.ratingKey, - ); - } else { - // season - episodes = await client.getUnwatchedEpisodesInSeason( - widget.metadata.ratingKey, - ); - } - - // Close loading indicator - if (context.mounted) { - Navigator.pop(context); - } - - if (episodes.isEmpty) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No unwatched episodes found')), - ); - } - return; - } - - // Shuffle the episodes - episodes.shuffle(); - - // Store shuffle queue in provider - playbackState.setShuffleQueue(episodes, widget.metadata.ratingKey); - - // Navigate to first episode - if (context.mounted) { - await navigateToVideoPlayer(context, metadata: episodes.first); - } - } catch (e) { - // Close loading indicator if it's still open - if (context.mounted && Navigator.canPop(context)) { - Navigator.pop(context); - } - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error starting shuffle play: $e')), - ); - } - } - } @override Widget build(BuildContext context) {