import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/connectivity_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/key_listener.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/settings/widgets/settings_message_box.dart'; import 'package:fladder/screens/settings/widgets/subtitle_editor.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/bitrate_helper.dart'; import 'package:fladder/util/box_fit_extension.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; @RoutePage() class PlayerSettingsPage extends ConsumerStatefulWidget { const PlayerSettingsPage({super.key}); @override ConsumerState createState() => _PlayerSettingsPageState(); } class _PlayerSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final videoSettings = ref.watch(videoPlayerSettingsProvider); final provider = ref.read(videoPlayerSettingsProvider.notifier); final connectionState = ref.watch(connectivityStatusProvider); final userSettings = ref.watch(userProvider.select((value) => value?.userSettings)); return SettingsScaffold( label: context.localized.settingsPlayerTitle, items: [ ...settingsListGroup( context, SettingsLabelDivider(label: context.localized.video), [ if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) Column( children: [ SettingsListTile( label: Text(context.localized.videoScalingFillScreenTitle), subLabel: Text(context.localized.videoScalingFillScreenDesc), onTap: () => provider.setFillScreen(!videoSettings.fillScreen), trailing: Switch( value: videoSettings.fillScreen, onChanged: (value) => provider.setFillScreen(value), ), ), AnimatedFadeSize( child: videoSettings.fillScreen ? SettingsMessageBox( context.localized.videoScalingFillScreenNotif, messageType: MessageType.warning, ) : Container(), ), ], ), SettingsListTile( label: Text(context.localized.videoScaling), trailing: EnumBox( current: videoSettings.videoFit.label(context), itemBuilder: (context) => BoxFit.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry), ), ) .toList(), ), ), SettingsListTile( label: _StatusIndicator( homeInternet: connectionState.homeInternet, label: Text(context.localized.homeStreamingQualityTitle), ), subLabel: Text(context.localized.homeStreamingQualityDesc), trailing: EnumBox( current: ref.watch( videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(maxHomeBitrate: entry), ), ) .toList(), ), ), SettingsListTile( label: _StatusIndicator( homeInternet: !connectionState.homeInternet, label: Text(context.localized.internetStreamingQualityTitle), ), subLabel: Text(context.localized.internetStreamingQualityDesc), trailing: EnumBox( current: ref.watch( videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(maxInternetBitrate: entry), ), ) .toList(), ), ), ], ), const SizedBox(height: 12), ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( (entry) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Expanded( child: Text( entry.key.label(context), style: Theme.of(context).textTheme.titleLarge, ), ), EnumBox( current: entry.value.label(context), itemBuilder: (context) => SegmentSkip.values .map( (value) => ItemActionButton( label: Text(value.label(context)), action: () { final newEntries = videoSettings.segmentSkipSettings.map( (key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue)); ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(segmentSkipSettings: newEntries); }, ), ) .toList(), ) ], ), ), ), ]), const SizedBox(height: 12), ...settingsListGroup( context, SettingsLabelDivider(label: context.localized.shortCuts), [ if (userSettings != null) SettingsListTile( label: Text(context.localized.skipBackLength), trailing: SizedBox( width: 125, child: IntInputField( suffix: context.localized.seconds(10), controller: TextEditingController(text: userSettings.skipBackDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setBackwardSpeed(value); } }, )), ), SettingsListTile( label: Text(context.localized.skipForwardLength), trailing: SizedBox( width: 125, child: IntInputField( suffix: context.localized.seconds(10), controller: TextEditingController(text: userSettings!.skipForwardDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setForwardSpeed(value); } }, )), ), if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ExpansionTile( title: Text( context.localized.keyboardShortCuts, style: Theme.of(context).textTheme.titleLarge, ), children: VideoHotKeys.values .map( (entry) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Expanded( child: Text( entry.label(context), style: Theme.of(context).textTheme.titleMedium, ), ), Flexible( child: KeyCombinationWidget( currentKey: videoSettings.hotKeys[entry], defaultKey: videoSettings.defaultShortCuts[entry]!, onChanged: (value) => ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), ), ), ], ), ), ) .toList(), ), ], ), const SizedBox(height: 12), ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [ SettingsListTile( label: Text(context.localized.rememberAudioSelections), subLabel: Text(context.localized.rememberAudioSelectionsDesc), onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(), trailing: Switch( value: ref.watch(userProvider.select( (value) => value?.userConfiguration?.rememberAudioSelections ?? true, )), onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(), ), ), SettingsListTile( label: Text(context.localized.rememberSubtitleSelections), subLabel: Text(context.localized.rememberSubtitleSelectionsDesc), onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(), trailing: Switch( value: ref.watch(userProvider.select( (value) => value?.userConfiguration?.rememberSubtitleSelections ?? true, )), onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), ), ), ]), const SizedBox(height: 12), ...settingsListGroup( context, SettingsLabelDivider(label: context.localized.advanced), [ if (PlayerOptions.available.length != 1) SettingsListTile( label: Text(context.localized.playerSettingsBackendTitle), subLabel: Text(context.localized.playerSettingsBackendDesc), trailing: Builder(builder: (context) { final wantedPlayer = videoSettings.wantedPlayer; final currentPlayer = videoSettings.playerOptions; return EnumBox( current: currentPlayer == null ? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})" : wantedPlayer.label(context), itemBuilder: (context) => [ ItemActionButton( label: Text( "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(playerOptions: null), ), ...PlayerOptions.available.map( (entry) => ItemActionButton( label: Text(entry.label(context)), action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(playerOptions: entry), ), ) ], ); }), ), AnimatedFadeSize( child: switch (videoSettings.wantedPlayer) { PlayerOptions.libMPV => Column( children: [ SettingsListTile( label: Text(context.localized.settingsPlayerVideoHWAccelTitle), subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc), onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel), trailing: Switch( value: videoSettings.hardwareAccel, onChanged: (value) => provider.setHardwareAccel(value), ), ), if (!kIsWeb) SettingsListTile( label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), onTap: () => provider.setUseLibass(!videoSettings.useLibass), trailing: Switch( value: videoSettings.useLibass, onChanged: (value) => provider.setUseLibass(value), ), ), if (!videoSettings.useLibass) SettingsListTile( label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc), onTap: videoSettings.useLibass ? null : () { showDialog( context: context, barrierDismissible: true, useSafeArea: false, builder: (context) => const SubtitleEditor(), ); }, ), AnimatedFadeSize( child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid ? SettingsMessageBox( context.localized.settingsPlayerMobileWarning, messageType: MessageType.warning, ) : Container(), ), SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), trailing: SizedBox( width: 70, child: IntInputField( suffix: 'MB', controller: TextEditingController(text: videoSettings.bufferSize.toString()), onSubmitted: (value) { if (value != null) { provider.setBufferSize(value); } }, )), ), ], ), PlayerOptions.nativePlayer => Column( children: [ SettingsListTile( label: Text(context.localized.mediaTunnelingTitle), subLabel: Text(context.localized.mediaTunnelingDesc), onTap: () => provider.setMediaTunneling(!videoSettings.enableTunneling), trailing: Switch( value: videoSettings.enableTunneling, onChanged: (value) => provider.setMediaTunneling(value), ), ), ], ), PlayerOptions.libMDK => SettingsMessageBox( messageType: MessageType.info, "${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"), }, ), Column( children: [ SettingsListTile( label: Text(context.localized.settingsAutoNextTitle), subLabel: Text(context.localized.settingsAutoNextDesc), trailing: EnumBox( current: ref.watch( videoPlayerSettingsProvider.select( (value) => value.nextVideoType.label(context), ), ), itemBuilder: (context) => AutoNextType.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(nextVideoType: entry), ), ) .toList(), ), ), AnimatedFadeSize( child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), _ => const SizedBox.shrink(), }, ), ], ), if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode) SettingsListTile( label: Text(context.localized.playerSettingsOrientationTitle), subLabel: Text(context.localized.playerSettingsOrientationDesc), onTap: () => showOrientationOptions(context, ref), ), ], ), ], ); } } class _StatusIndicator extends StatelessWidget { final bool homeInternet; final Widget label; const _StatusIndicator({required this.homeInternet, required this.label}); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (homeInternet) ...[ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.greenAccent, shape: BoxShape.circle, ), ), const SizedBox(width: 6), ], Flexible(child: label), ], ); } }