import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/playback/direct_playback_model.dart'; import 'package:fladder/models/playback/offline_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/playback/transcode_playback_model.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/collections/add_to_collection.dart'; import 'package:fladder/screens/metadata/info_screen.dart'; import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/spaced_list_tile.dart'; Future showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) { return showBottomSheetPill( context: context, content: (context, scrollController) { return VideoOptions( controller: scrollController, minimizePlayer: minimizePlayer, ); }, ); } class VideoOptions extends ConsumerStatefulWidget { final ScrollController controller; final Function() minimizePlayer; const VideoOptions({required this.controller, required this.minimizePlayer, super.key}); @override ConsumerState createState() => _VideoOptionsMobileState(); } class _VideoOptionsMobileState extends ConsumerState { late int page = 0; @override Widget build(BuildContext context) { final currentItem = ref.watch(playBackModel.select((value) => value?.item)); final videoSettings = ref.watch(videoPlayerSettingsProvider); final currentMediaStreams = ref.watch(playBackModel.select((value) => value?.mediaStreams)); Widget mainPage() { return ListView( key: const Key("mainPage"), shrinkWrap: true, controller: widget.controller, children: [ InkWell( onTap: () => setState(() => page = 2), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( currentItem?.title ?? "", style: Theme.of(context).textTheme.titleLarge, ), ], ), const Spacer(), const Opacity(opacity: 0.1, child: Icon(Icons.info_outline_rounded)) ], ), ), ), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), if (!AdaptiveLayout.of(context).isDesktop) ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Flexible(flex: 1, child: Text("Screen Brightness")), Flexible( child: Row( children: [ Flexible( child: Opacity( opacity: videoSettings.screenBrightness == null ? 0.5 : 1, child: Slider( value: videoSettings.screenBrightness ?? 1.0, min: 0, max: 1, onChanged: (value) => ref.read(videoPlayerSettingsProvider.notifier).setScreenBrightness(value), ), ), ), IconButton( onPressed: () => ref.read(videoPlayerSettingsProvider.notifier).setScreenBrightness(null), icon: Opacity( opacity: videoSettings.screenBrightness != null ? 0.5 : 1, child: Icon( IconsaxBold.autobrightness, color: Theme.of(context).colorScheme.primary, ), ), ) ], ), ), ], ), ), SpacedListTile( title: const Text("Subtitles"), content: Text(currentMediaStreams?.currentSubStream?.displayTitle ?? "Off"), onTap: currentMediaStreams?.subStreams.isNotEmpty == true ? () => showSubSelection(context) : null, ), SpacedListTile( title: const Text("Audio"), content: Text(currentMediaStreams?.currentAudioStream?.displayTitle ?? "Off"), onTap: currentMediaStreams?.audioStreams.isNotEmpty == true ? () => showAudioSelection(context) : null, ), ListTile( title: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: EnumSelection( label: const Text("Scale"), current: videoSettings.videoFit.name.toUpperCaseSplit(), itemBuilder: (context) => BoxFit.values .map((value) => PopupMenuItem( value: value, child: Text(value.name.toUpperCaseSplit()), onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(value), )) .toList(), ), ), ], ), ), if (!AdaptiveLayout.of(context).isDesktop) ListTile( onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFillScreen(!videoSettings.fillScreen), title: Row( mainAxisSize: MainAxisSize.min, children: [ const Expanded( flex: 3, child: Text("Fill-screen"), ), const Spacer(), Switch.adaptive( value: videoSettings.fillScreen, onChanged: (value) => ref.read(videoPlayerSettingsProvider.notifier).setFillScreen(value), ) ], ), ), ], ); } Widget playbackSettings() { final playbackState = ref.watch(playBackModel); return ListView( key: const Key("PlaybackSettings"), shrinkWrap: true, controller: widget.controller, children: [ navTitle("Playback Settings", null), if (playbackState?.queue.isNotEmpty == true) ListTile( leading: const Icon(Icons.video_collection_rounded), title: const Text("Show queue"), onTap: () { Navigator.of(context).pop(); ref.read(videoPlayerProvider).pause(); showFullScreenItemQueue( context, items: playbackState?.queue ?? [], currentItem: playbackState?.item, playSelected: (item) { throw UnimplementedError(); }, ); }, ) ], ); } return Column( children: [ AnimatedSize( duration: const Duration(milliseconds: 250), child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: switch (page) { 1 => playbackSettings(), 2 => itemInfo(currentItem, context), _ => mainPage(), }, ), ), const SizedBox(height: 16), ], ); } ListView itemInfo(ItemBaseModel? currentItem, BuildContext context) { return ListView( shrinkWrap: true, children: [ navTitle(currentItem?.title, currentItem?.subTextShort(context)), if (currentItem != null) ...{ if (currentItem.type == FladderItemType.episode) ListTile( onTap: () { Navigator.of(context).pop(); widget.minimizePlayer(); (this as EpisodeModel).parentBaseModel.navigateTo(context); }, title: const Text("Open show"), ), ListTile( onTap: () async { Navigator.of(context).pop(); widget.minimizePlayer(); await currentItem.navigateTo(context); }, title: const Text("Show details"), ), if (currentItem.type != FladderItemType.boxset) ListTile( onTap: () async { await addItemToCollection(context, [currentItem]); if (context.mounted) { context.refreshData(); } }, title: const Text("Add to collection"), ), if (currentItem.type != FladderItemType.playlist) ListTile( onTap: () async { await addItemToPlaylist(context, [currentItem]); if (context.mounted) { context.refreshData(); } }, title: const Text("Add to playlist"), ), ListTile( onTap: () async { final response = await ref .read(userProvider.notifier) .setAsFavorite(!(currentItem.userData.isFavourite == true), currentItem.id); final newItem = currentItem.copyWith(userData: response?.body); final playbackModel = switch (ref.read(playBackModel)) { DirectPlaybackModel value => value.copyWith(item: newItem), TranscodePlaybackModel value => value.copyWith(item: newItem), OfflinePlaybackModel value => value.copyWith(item: newItem), _ => null }; ref.read(playBackModel.notifier).update((state) => playbackModel); Navigator.of(context).pop(); }, title: Text(currentItem.userData.isFavourite == true ? "Remove from favorites" : "Add to favourites"), ), ListTile( onTap: () { Navigator.of(context).pop(); showInfoScreen(context, currentItem); }, title: const Text('Media info'), ), } ], ); } Widget navTitle(String? title, String? subText) { return Column( children: [ Row( children: [ const SizedBox(width: 8), BackButton( onPressed: () => setState(() => page = 0), ), const SizedBox(width: 16), Column( children: [ if (title != null) Text( title, style: Theme.of(context).textTheme.titleLarge, ), if (subText != null) Text( subText, style: Theme.of(context).textTheme.titleMedium, ) ], ), ], ), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), ], ); } } Future showSubSelection(BuildContext context) { return showDialog( context: context, builder: (context) { return Consumer( builder: (context, ref, child) { final playbackModel = ref.watch(playBackModel); final player = ref.watch(videoPlayerProvider); return SimpleDialog( contentPadding: const EdgeInsets.only(top: 8, bottom: 24), title: Row( children: [ const Text("Subtitle"), const Spacer(), IconButton.outlined( onPressed: () { Navigator.pop(context); showSubtitleControls( context: context, label: 'Subtitle configuration', ); }, icon: const Icon(Icons.display_settings_rounded)) ], ), children: playbackModel?.subStreams?.mapIndexed( (index, subModel) { final selected = playbackModel.mediaStreams?.defaultSubStreamIndex == subModel.index; return ListTile( title: Text(subModel.displayTitle), tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, subtitle: subModel.language.isNotEmpty ? Opacity(opacity: 0.6, child: Text(subModel.language.capitalize())) : null, onTap: () async { final newModel = await playbackModel.setSubtitle(subModel, player); ref.read(playBackModel.notifier).update((state) => newModel); if (newModel != null) { await ref.read(playbackModelHelper).shouldReload(newModel); } }, ); }, ).toList(), ); }, ); }, ); } Future showAudioSelection(BuildContext context) { return showDialog( context: context, builder: (context) { return Consumer( builder: (context, ref, child) { final playbackModel = ref.watch(playBackModel); final player = ref.watch(videoPlayerProvider); return SimpleDialog( contentPadding: const EdgeInsets.only(top: 8, bottom: 24), title: Row( children: [ const Text("Subtitle"), const Spacer(), IconButton.outlined( onPressed: () { Navigator.pop(context); showSubtitleControls( context: context, label: 'Subtitle configuration', ); }, icon: const Icon(Icons.display_settings_rounded)) ], ), children: playbackModel?.audioStreams?.mapIndexed( (index, audioStream) { final selected = playbackModel.mediaStreams?.defaultAudioStreamIndex == audioStream.index; return ListTile( title: Text(audioStream.displayTitle), tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, subtitle: audioStream.language.isNotEmpty ? Opacity(opacity: 0.6, child: Text(audioStream.language.capitalize())) : null, onTap: () async { final newModel = await playbackModel.setAudio(audioStream, player); ref.read(playBackModel.notifier).update((state) => newModel); if (newModel != null) { await ref.read(playbackModelHelper).shouldReload(newModel); } }); }, ).toList(), ); }, ); }, ); }