import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/music_playback_deck.dart'; import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; import 'package:spotiflac_android/widgets/app_sliver_header.dart'; import 'package:spotiflac_android/widgets/mornye_volume_control.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; class PlaybackSettingsPage extends ConsumerStatefulWidget { const PlaybackSettingsPage({super.key}); @override ConsumerState createState() => _PlaybackSettingsPageState(); } class _PlaybackSettingsPageState extends ConsumerState { Future _confirmAudioVolume(String message) async { return await showAppDialog( context: context, builder: (context) => AppAlertDialog( title: Text(context.l10n.usbVolumeWarningTitle), content: Text(message), actions: [ AppDialogAction( onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.dialogCancel), ), AppDialogAction( onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.usbVolumeAccept), ), ], ), ) ?? false; } @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: CustomScrollView( slivers: [ AppSliverHeader.page(title: context.l10n.libraryPlayback), SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionPlayer), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsItem( icon: Icons.open_in_new, title: context.l10n.libraryExternalPlayer, subtitle: context.l10n.libraryExternalPlayerSubtitle, trailing: settings.playerMode == 'external' ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () => ref .read(settingsProvider.notifier) .setPlayerMode('external'), ), SettingsItem( icon: Icons.play_circle_outline, title: context.l10n.libraryBuiltInPreviewPlayer, subtitle: context.l10n.libraryBuiltInPreviewPlayerSubtitle, trailing: settings.playerMode == 'internal' ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () => ref .read(settingsProvider.notifier) .setPlayerMode('internal'), showDivider: false, ), ], ), ), SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.sectionVolumeAndTransitions, ), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.graphic_eq, title: context.l10n.libraryPlaybackNormalization, subtitle: context.l10n.libraryPlaybackNormalizationSubtitle, value: settings.playbackNormalization, enabled: !settings.usbBitPerfect, onChanged: (value) => ref .read(settingsProvider.notifier) .setPlaybackNormalization(value), ), SettingsSwitchItem( icon: Icons.compare_arrows, title: 'AutoMix', subtitle: context.l10n.autoMixDescription, value: settings.autoMix, enabled: !settings.usbBitPerfect, onChanged: (value) => ref.read(settingsProvider.notifier).setAutoMix(value), showDivider: false, ), ], ), ), if (Platform.isAndroid) SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.sectionAudioOutput, ), ), if (Platform.isAndroid) SliverToBoxAdapter( child: SettingsGroup( children: [ ValueListenableBuilder( valueListenable: usbAudioStatus, builder: (context, status, _) => SettingsSwitchItem( icon: Icons.usb, title: context.l10n.usbBitPerfect, titleTrailing: const _PlaybackBetaBadge(), subtitle: [ context.l10n.usbBitPerfectDescription, if (settings.usbBitPerfect) switch (status.reason) { 'active' => '${status.device} · ${status.transport == 'pcm' ? '${status.bitDepth}-bit PCM' : status.transport == 'dop' ? 'DSD (DoP)' : 'DSD (Native)'} · ${(status.sampleRate! / 1000).toStringAsFixed(1)} kHz', 'dsd_unsupported' => context.l10n.usbDsdUnsupported, 'volume_unavailable' => context.l10n.usbVolumeUnavailable, 'exclusive_unavailable' => context.l10n.dapExclusiveUnavailable, 'permission_denied' => context.l10n.usbPermissionDenied, 'no_usb' => context.l10n.usbBitPerfectNoDevice, 'route_changed' => context.l10n.usbBitPerfectRouteChanged, 'android_version' || 'unsupported' || 'format' => context.l10n.usbBitPerfectFallback, _ => context.l10n.usbBitPerfectPending, }, ].join('\n\n'), value: settings.usbBitPerfect, onChanged: (value) async { if (value && !await _confirmAudioVolume( context.l10n.usbVolumeWarning, )) { return; } if (mounted) { ref .read(settingsProvider.notifier) .setUsbBitPerfect(value); } }, showDivider: settings.usbBitPerfect, ), ), if (settings.usbBitPerfect) ...[ SettingsSwitchItem( icon: Icons.usb, title: context.l10n.usbDirect, subtitle: context.l10n.usbDirectDescription, value: settings.usbDirect, onChanged: (value) async { if (value && !await _confirmAudioVolume( context.l10n.usbVolumeWarning, )) { return; } if (mounted) { ref .read(settingsProvider.notifier) .setUsbDirect(value); } }, ), if (settings.usbDirect) ...[ SettingsSwitchItem( icon: Icons.graphic_eq, title: context.l10n.usbDsdOverPcm, subtitle: context.l10n.usbDsdOverPcmDescription, value: settings.usbDsdOverPcm, onChanged: (value) => ref .read(settingsProvider.notifier) .setUsbDsdOverPcm(value), ), SettingsSwitchItem( icon: Icons.volume_up_outlined, title: context.l10n.usbFixedVolume, subtitle: context.l10n.usbFixedVolumeDescription, value: settings.usbAllowFixedVolume, onChanged: (value) async { if (value && !await _confirmAudioVolume( context.l10n.usbFixedVolumeWarning, )) { return; } if (mounted) { ref .read(settingsProvider.notifier) .setUsbAllowFixedVolume(value); } }, ), ValueListenableBuilder( valueListenable: usbHardwareVolume, builder: (context, volume, _) => Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.usbHardwareVolume, style: Theme.of(context).textTheme.titleMedium, ), Text( volume == null ? context.l10n.usbBitPerfectPending : volume.available ? '${volume.currentDb.toStringAsFixed(1)} dB' : context.l10n.usbVolumeUnavailable, ), if (volume?.available == true) MornyeVolumeControl( foreground: colorScheme.onSurface, ), ], ), ), ), ], SettingsSwitchItem( icon: Icons.headphones, title: context.l10n.dapExclusive, titleTrailing: const _PlaybackBetaBadge(), subtitle: context.l10n.dapExclusiveDescription, value: settings.dapExclusive, onChanged: (value) => ref .read(settingsProvider.notifier) .setDapExclusive(value), showDivider: false, ), ], ], ), ), const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), ); } } class _PlaybackBetaBadge extends StatelessWidget { const _PlaybackBetaBadge(); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Text( 'Beta', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colors.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ); } }