mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
feat(player): add AAudio hi-res, WavPack DSD and DAC volume
This commit is contained in:
1 parent
6decce0f8b
commit
d92bcf7716
32 files changed
+2118
-55
No files matched your search
@@ -26,6 +26,7 @@ import 'package:spotiflac_android/screens/settings/settings_tab.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/services/shell_navigation_service.dart';
|
||||
import 'package:spotiflac_android/services/share_intent_service.dart';
|
||||
import 'package:spotiflac_android/services/music_playback_deck.dart';
|
||||
import 'package:spotiflac_android/services/music_player_service.dart';
|
||||
import 'package:spotiflac_android/services/notification_service.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
@@ -63,6 +64,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
bool _hasCheckedAppAnnouncement = false;
|
||||
bool _initialSafRepairComplete = false;
|
||||
bool _safRepairDialogVisible = false;
|
||||
bool _usbVolumeWarningShown = false;
|
||||
StreamSubscription<String>? _shareSubscription;
|
||||
DateTime? _lastBackPress;
|
||||
final GlobalKey<NavigatorState> _homeTabNavigatorKey =
|
||||
@@ -96,6 +98,8 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
setUsbOutputOptions(
|
||||
direct: ref.read(settingsProvider).usbDirect,
|
||||
allowDop: ref.read(settingsProvider).usbDsdOverPcm,
|
||||
allowFixedVolume: ref.read(settingsProvider).usbAllowFixedVolume,
|
||||
dapExclusive: ref.read(settingsProvider).dapExclusive,
|
||||
);
|
||||
setAutoplayEnabled(
|
||||
ref.read(settingsProvider).autoplay,
|
||||
@@ -114,6 +118,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
usbAudioStatus.addListener(_onUsbAudioStatusChanged);
|
||||
_homePreviewStopObserver = _PreviewStopNavigatorObserver(
|
||||
() => ref.read(previewPlayerProvider.notifier).stop(),
|
||||
);
|
||||
@@ -158,6 +163,28 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
});
|
||||
}
|
||||
|
||||
void _onUsbAudioStatusChanged() {
|
||||
if (usbAudioStatus.value.reason == 'active') {
|
||||
_usbVolumeWarningShown = false;
|
||||
}
|
||||
if (usbAudioStatus.value.reason != 'volume_unavailable' ||
|
||||
_usbVolumeWarningShown) {
|
||||
return;
|
||||
}
|
||||
_usbVolumeWarningShown = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || usbAudioStatus.value.reason != 'volume_unavailable') {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.usbVolumeUnavailable),
|
||||
duration: const Duration(seconds: 8),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && _initialSafRepairComplete) {
|
||||
@@ -509,6 +536,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
usbAudioStatus.removeListener(_onUsbAudioStatusChanged);
|
||||
ShellNavigationService.unregisterTabSelectionHandler(this);
|
||||
_shareSubscription?.cancel();
|
||||
_pageController.dispose();
|
||||
@@ -751,8 +779,20 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
setUsbBitPerfectEnabled(enabled);
|
||||
});
|
||||
ref.listen(
|
||||
settingsProvider.select((s) => (s.usbDirect, s.usbDsdOverPcm)),
|
||||
(_, value) => setUsbOutputOptions(direct: value.$1, allowDop: value.$2),
|
||||
settingsProvider.select(
|
||||
(s) => (
|
||||
s.usbDirect,
|
||||
s.usbDsdOverPcm,
|
||||
s.usbAllowFixedVolume,
|
||||
s.dapExclusive,
|
||||
),
|
||||
),
|
||||
(_, value) => setUsbOutputOptions(
|
||||
direct: value.$1,
|
||||
allowDop: value.$2,
|
||||
allowFixedVolume: value.$3,
|
||||
dapExclusive: value.$4,
|
||||
),
|
||||
);
|
||||
ref.listen(
|
||||
settingsProvider.select((s) => (s.autoplay, s.localLibraryEnabled)),
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
import 'package:spotiflac_android/widgets/ios_library_folder_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_volume_control.dart';
|
||||
|
||||
class LibrarySettingsPage extends ConsumerStatefulWidget {
|
||||
const LibrarySettingsPage({super.key});
|
||||
@@ -205,6 +206,27 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
await ref.read(localLibraryProvider.notifier).cancelScan();
|
||||
}
|
||||
|
||||
Future<bool> _confirmAudioVolume(String message) async {
|
||||
return await showAppDialog<bool>(
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _clearLibrary() async {
|
||||
final confirmed = await showAppDialog<bool>(
|
||||
context: context,
|
||||
@@ -913,6 +935,10 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
? '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,
|
||||
@@ -925,9 +951,19 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
},
|
||||
].join('\n\n'),
|
||||
value: settings.usbBitPerfect,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setUsbBitPerfect(value),
|
||||
onChanged: (value) async {
|
||||
if (value &&
|
||||
!await _confirmAudioVolume(
|
||||
context.l10n.usbVolumeWarning,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (mounted) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setUsbBitPerfect(value);
|
||||
}
|
||||
},
|
||||
showDivider: settings.usbBitPerfect,
|
||||
),
|
||||
),
|
||||
@@ -937,9 +973,18 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
title: context.l10n.usbDirect,
|
||||
subtitle: context.l10n.usbDirectDescription,
|
||||
value: settings.usbDirect,
|
||||
onChanged: (value) =>
|
||||
ref.read(settingsProvider.notifier).setUsbDirect(value),
|
||||
showDivider: settings.usbDirect,
|
||||
onChanged: (value) async {
|
||||
if (value &&
|
||||
!await _confirmAudioVolume(
|
||||
context.l10n.usbVolumeWarning,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
if (mounted) {
|
||||
ref.read(settingsProvider.notifier).setUsbDirect(value);
|
||||
}
|
||||
},
|
||||
showDivider: true,
|
||||
),
|
||||
if (settings.usbDirect)
|
||||
SettingsSwitchItem(
|
||||
@@ -950,8 +995,70 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setUsbDsdOverPcm(value),
|
||||
showDivider: false,
|
||||
showDivider: true,
|
||||
),
|
||||
if (settings.usbDirect) ...[
|
||||
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<UsbVolumeState?>(
|
||||
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: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.headphones,
|
||||
title: context.l10n.dapExclusive,
|
||||
subtitle: context.l10n.dapExclusiveDescription,
|
||||
value: settings.dapExclusive,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDapExclusive(value),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user