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? onPickerChanged; @override State createState() => _AudioOutputButtonState(); } class _AudioOutputButtonState extends State { static const _androidChannel = MethodChannel( 'com.zarz.spotiflac/audio_output', ); MethodChannel? _viewChannel; bool _opening = false; Map 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('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 _showAndroidPicker() async { if (_opening) return; _opening = true; widget.onPickerChanged?.call(true); var opened = false; try { opened = await _androidChannel.invokeMethod('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(); } } }