From daff76363b130a37cd85a6d5f5b123e4a59034b9 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:29:49 +0100 Subject: [PATCH] refactor(tv): clean up bottom sheet --- .../livetv/live_tv_show_schedule_screen.dart | 130 +--------------- lib/screens/livetv/program_details_sheet.dart | 140 ++++++++++++++++++ lib/screens/livetv/tabs/whats_on_tab.dart | 119 +-------------- 3 files changed, 154 insertions(+), 235 deletions(-) create mode 100644 lib/screens/livetv/program_details_sheet.dart diff --git a/lib/screens/livetv/live_tv_show_schedule_screen.dart b/lib/screens/livetv/live_tv_show_schedule_screen.dart index 5d6f8d04..f618e00e 100644 --- a/lib/screens/livetv/live_tv_show_schedule_screen.dart +++ b/lib/screens/livetv/live_tv_show_schedule_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import '../../i18n/strings.g.dart'; import '../../models/livetv_channel.dart'; import '../../models/livetv_program.dart'; import '../../providers/multi_server_provider.dart'; @@ -10,8 +9,8 @@ import '../../theme/mono_tokens.dart'; import '../../utils/formatters.dart'; import '../../utils/live_tv_player_navigation.dart'; import '../../utils/plex_image_helper.dart'; -import '../../widgets/app_icon.dart'; import '../../widgets/focused_scroll_scaffold.dart'; +import 'program_details_sheet.dart'; /// Shows all upcoming airings of a show, matching the Plex "upcoming episodes" view. class LiveTvShowScheduleScreen extends StatefulWidget { @@ -106,8 +105,6 @@ class _LiveTvShowScheduleScreenState extends State { } void _showProgramDetails(LiveTvProgram program, LiveTvChannel? channel) { - final theme = Theme.of(context); - final multiServer = context.read(); final client = multiServer.getClientForServer(widget.serverId); String? posterUrl; @@ -122,125 +119,12 @@ class _LiveTvShowScheduleScreenState extends State { ); } - showModalBottomSheet( - context: context, - builder: (sheetContext) { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (posterUrl != null) ...[ - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - posterUrl, - width: 80, - height: 120, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - const SizedBox(width: 14), - ], - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - program.displayTitle, - style: theme.textTheme.titleMedium, - ), - ), - if (program.isCurrentlyAiring) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - t.liveTv.live, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 11, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - [ - if (channel != null) channel.displayName, - if (program.startTime != null && program.endTime != null) - '${program.startTime!.hour.toString().padLeft(2, '0')}:${program.startTime!.minute.toString().padLeft(2, '0')} - ${program.endTime!.hour.toString().padLeft(2, '0')}:${program.endTime!.minute.toString().padLeft(2, '0')}', - if (program.durationMinutes > 0) formatDurationTextual(program.durationMinutes * 60000), - ].join(' · '), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (program.summary != null && program.summary!.isNotEmpty) ...[ - const SizedBox(height: 12), - Text( - program.summary!, - style: theme.textTheme.bodyMedium, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - if (program.isCurrentlyAiring && channel != null) - FilledButton.icon( - onPressed: () { - Navigator.of(sheetContext).pop(); - _tuneChannel(channel); - }, - icon: const AppIcon(Symbols.play_arrow_rounded), - label: Text(t.common.play), - ), - if (program.isCurrentlyAiring) const SizedBox(width: 8), - OutlinedButton.icon( - onPressed: () { - Navigator.of(sheetContext).pop(); - // TODO: Record action - }, - icon: const AppIcon(Symbols.fiber_manual_record_rounded), - label: Text(t.liveTv.record), - ), - if (!program.isCurrentlyAiring && channel != null) ...[ - const SizedBox(width: 8), - OutlinedButton.icon( - onPressed: () { - Navigator.of(sheetContext).pop(); - _tuneChannel(channel); - }, - icon: const AppIcon(Symbols.live_tv_rounded), - label: Text(t.liveTv.watchChannel), - ), - ], - ], - ), - ], - ), - ); - }, + showProgramDetailsSheet( + context, + program: program, + channel: channel, + posterUrl: posterUrl, + onTuneChannel: channel != null ? () => _tuneChannel(channel) : null, ); } diff --git a/lib/screens/livetv/program_details_sheet.dart b/lib/screens/livetv/program_details_sheet.dart new file mode 100644 index 00000000..1eac2144 --- /dev/null +++ b/lib/screens/livetv/program_details_sheet.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../../i18n/strings.g.dart'; +import '../../models/livetv_channel.dart'; +import '../../models/livetv_program.dart'; +import '../../utils/formatters.dart'; +import '../../widgets/app_icon.dart'; + +/// Shows a bottom sheet with program details and actions (Record, Watch Channel, Play). +void showProgramDetailsSheet( + BuildContext context, { + required LiveTvProgram program, + required LiveTvChannel? channel, + required String? posterUrl, + required VoidCallback? onTuneChannel, +}) { + final theme = Theme.of(context); + + showModalBottomSheet( + context: context, + builder: (sheetContext) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (posterUrl != null) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.network( + posterUrl, + width: 80, + height: 120, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + const SizedBox(width: 14), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + program.displayTitle, + style: theme.textTheme.titleMedium, + ), + ), + if (program.isCurrentlyAiring) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + t.liveTv.live, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + [ + if (channel != null) channel.displayName, + if (program.startTime != null && program.endTime != null) + '${program.startTime!.hour.toString().padLeft(2, '0')}:${program.startTime!.minute.toString().padLeft(2, '0')} - ${program.endTime!.hour.toString().padLeft(2, '0')}:${program.endTime!.minute.toString().padLeft(2, '0')}', + if (program.durationMinutes > 0) formatDurationTextual(program.durationMinutes * 60000), + ].join(' · '), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + if (program.summary != null && program.summary!.isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + program.summary!, + style: theme.textTheme.bodyMedium, + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + if (program.isCurrentlyAiring && onTuneChannel != null) + FilledButton.icon( + onPressed: () { + Navigator.of(sheetContext).pop(); + onTuneChannel(); + }, + icon: const AppIcon(Symbols.play_arrow_rounded), + label: Text(t.common.play), + ), + if (program.isCurrentlyAiring) const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: () { + Navigator.of(sheetContext).pop(); + // TODO: Record action + }, + icon: const AppIcon(Symbols.fiber_manual_record_rounded), + label: Text(t.liveTv.record), + ), + if (!program.isCurrentlyAiring && onTuneChannel != null) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: () { + Navigator.of(sheetContext).pop(); + onTuneChannel(); + }, + icon: const AppIcon(Symbols.live_tv_rounded), + label: Text(t.liveTv.watchChannel), + ), + ], + ], + ), + ], + ), + ); + }, + ); +} diff --git a/lib/screens/livetv/tabs/whats_on_tab.dart b/lib/screens/livetv/tabs/whats_on_tab.dart index 88e08843..76d63d6a 100644 --- a/lib/screens/livetv/tabs/whats_on_tab.dart +++ b/lib/screens/livetv/tabs/whats_on_tab.dart @@ -4,16 +4,13 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import '../../../i18n/strings.g.dart'; import '../../../models/livetv_channel.dart'; import '../../../models/livetv_hub_result.dart'; -import '../../../models/livetv_program.dart'; import '../../../providers/multi_server_provider.dart'; import '../../../providers/settings_provider.dart'; import '../../../services/settings_service.dart' show LibraryDensity; import '../../../theme/mono_tokens.dart'; import '../../../utils/app_logger.dart'; -import '../../../utils/formatters.dart'; import '../../../utils/layout_constants.dart'; import '../../../utils/live_tv_player_navigation.dart'; import '../../../utils/plex_image_helper.dart'; @@ -22,6 +19,7 @@ import '../../../widgets/app_icon.dart'; import '../../../widgets/horizontal_scroll_with_arrows.dart'; import '../../../widgets/plex_optimized_image.dart'; import '../live_tv_show_schedule_screen.dart'; +import '../program_details_sheet.dart'; class WhatsOnTab extends StatefulWidget { final List channels; @@ -132,7 +130,6 @@ class _WhatsOnTabState extends State { } void _showProgramDetails(LiveTvHubEntry entry, LiveTvChannel? channel) { - final theme = Theme.of(context); final program = entry.program; final metadata = entry.metadata; @@ -151,114 +148,12 @@ class _WhatsOnTabState extends State { ); } - showModalBottomSheet( - context: context, - builder: (sheetContext) { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (posterUrl != null) ...[ - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - posterUrl, - width: 80, - height: 120, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - const SizedBox(width: 14), - ], - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - program.displayTitle, - style: theme.textTheme.titleMedium, - ), - ), - if (program.isCurrentlyAiring) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - t.liveTv.live, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 11, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - [ - if (channel != null) channel.displayName, - if (program.startTime != null && program.endTime != null) - '${program.startTime!.hour.toString().padLeft(2, '0')}:${program.startTime!.minute.toString().padLeft(2, '0')} - ${program.endTime!.hour.toString().padLeft(2, '0')}:${program.endTime!.minute.toString().padLeft(2, '0')}', - if (program.durationMinutes > 0) formatDurationTextual(program.durationMinutes * 60000), - ].join(' · '), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (program.summary != null && program.summary!.isNotEmpty) ...[ - const SizedBox(height: 12), - Text( - program.summary!, - style: theme.textTheme.bodyMedium, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - if (program.isCurrentlyAiring && channel != null) - FilledButton.icon( - onPressed: () { - Navigator.of(sheetContext).pop(); - _tuneChannel(channel); - }, - icon: const AppIcon(Symbols.play_arrow_rounded), - label: Text(t.common.play), - ), - const SizedBox(width: 8), - OutlinedButton.icon( - onPressed: () { - Navigator.of(sheetContext).pop(); - // TODO: Record action - }, - icon: const AppIcon(Symbols.fiber_manual_record_rounded), - label: Text(t.liveTv.record), - ), - ], - ), - ], - ), - ); - }, + showProgramDetailsSheet( + context, + program: program, + channel: channel, + posterUrl: posterUrl, + onTuneChannel: channel != null ? () => _tuneChannel(channel) : null, ); }