mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
feat(player): add native audio output controls
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
/// Uses the system's real audio routes and keeps native discovery in native UI.
|
||||
class AudioOutputButton extends StatefulWidget {
|
||||
const AudioOutputButton({super.key, this.color, this.onPickerChanged});
|
||||
|
||||
final Color? color;
|
||||
final ValueChanged<bool>? onPickerChanged;
|
||||
|
||||
@override
|
||||
State<AudioOutputButton> createState() => _AudioOutputButtonState();
|
||||
}
|
||||
|
||||
class _AudioOutputButtonState extends State<AudioOutputButton> {
|
||||
static const _androidChannel = MethodChannel(
|
||||
'com.zarz.spotiflac/audio_output',
|
||||
);
|
||||
MethodChannel? _viewChannel;
|
||||
bool _opening = false;
|
||||
|
||||
Map<String, Object> get _appearance => {
|
||||
'color': (widget.color ?? Theme.of(context).colorScheme.onSurface)
|
||||
.toARGB32(),
|
||||
'label': context.l10n.nowPlayingAudioOutput,
|
||||
};
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AudioOutputButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
void _updateAppearance() {
|
||||
final channel = _viewChannel;
|
||||
if (channel != null) {
|
||||
unawaited(channel.invokeMethod<void>('update', _appearance));
|
||||
}
|
||||
}
|
||||
|
||||
void _attachView(int id) {
|
||||
_viewChannel?.setMethodCallHandler(null);
|
||||
_viewChannel = MethodChannel('com.zarz.spotiflac/audio_output/$id')
|
||||
..setMethodCallHandler((call) async {
|
||||
if (mounted && call.method == 'pickerChanged') {
|
||||
widget.onPickerChanged?.call(call.arguments == true);
|
||||
}
|
||||
});
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
Future<void> _showAndroidPicker() async {
|
||||
if (_opening) return;
|
||||
_opening = true;
|
||||
widget.onPickerChanged?.call(true);
|
||||
var opened = false;
|
||||
try {
|
||||
opened = await _androidChannel.invokeMethod<bool>('show') ?? false;
|
||||
} on PlatformException {
|
||||
// Show a visible failure instead of leaving an unresponsive control.
|
||||
} on MissingPluginException {
|
||||
// The native bridge is not available on unsupported builds.
|
||||
} finally {
|
||||
_opening = false;
|
||||
if (mounted) widget.onPickerChanged?.call(false);
|
||||
}
|
||||
if (mounted && !opened) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.nowPlayingAudioOutputUnavailable)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewChannel?.setMethodCallHandler(null);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (kIsWeb) return const SizedBox.shrink();
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
return SizedBox.square(
|
||||
dimension: 48,
|
||||
child: UiKitView(
|
||||
viewType: 'com.zarz.spotiflac/audio_output',
|
||||
creationParams: _appearance,
|
||||
creationParamsCodec: const StandardMessageCodec(),
|
||||
onPlatformViewCreated: _attachView,
|
||||
),
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
return IconButton(
|
||||
tooltip: context.l10n.nowPlayingAudioOutput,
|
||||
color: widget.color,
|
||||
icon: const Icon(Icons.speaker_group_outlined),
|
||||
onPressed: _showAndroidPicker,
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/widgets/audio_output_button.dart';
|
||||
|
||||
/// Landscape opens on the player. Lyrics can hide their bottom actions until
|
||||
/// the user touches the player, while the cover and header stay in place.
|
||||
@@ -37,6 +38,7 @@ class MornyeLandscapePlayer extends StatefulWidget {
|
||||
class _MornyeLandscapePlayerState extends State<MornyeLandscapePlayer> {
|
||||
Timer? _hideTimer;
|
||||
bool _actionsVisible = true;
|
||||
bool _audioOutputOpen = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -62,6 +64,7 @@ class _MornyeLandscapePlayerState extends State<MornyeLandscapePlayer> {
|
||||
_hideTimer?.cancel();
|
||||
if (widget.page != 1 ||
|
||||
!_actionsVisible ||
|
||||
_audioOutputOpen ||
|
||||
MediaQuery.accessibleNavigationOf(context)) {
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +154,35 @@ class _MornyeLandscapePlayerState extends State<MornyeLandscapePlayer> {
|
||||
),
|
||||
child: widget.page == 2
|
||||
? widget.queue
|
||||
: widget.lyrics,
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
widget.lyrics,
|
||||
if (widget.lyricsOptions != null)
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: 52,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_actionsVisible,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _actionsVisible
|
||||
? 1
|
||||
: 0,
|
||||
duration:
|
||||
MediaQuery.disableAnimationsOf(
|
||||
context,
|
||||
)
|
||||
? Duration.zero
|
||||
: const Duration(
|
||||
milliseconds: 180,
|
||||
),
|
||||
child:
|
||||
widget.lyricsOptions!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -195,9 +226,15 @@ class _MornyeLandscapePlayerState extends State<MornyeLandscapePlayer> {
|
||||
widget.page == 1 ? 0 : 1,
|
||||
),
|
||||
),
|
||||
if (widget.page == 1 &&
|
||||
widget.lyricsOptions != null)
|
||||
widget.lyricsOptions!,
|
||||
AudioOutputButton(
|
||||
onPickerChanged: (open) {
|
||||
if (!mounted) return;
|
||||
setState(
|
||||
() => _audioOutputOpen = open,
|
||||
);
|
||||
_scheduleHide();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip:
|
||||
context.l10n.nowPlayingUpNext,
|
||||
|
||||
Reference in New Issue
Block a user