import 'package:audio_session/audio_session.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/theme.dart'; /// Where music plays right now, as the phone reports it. class OutputDevice { const OutputDevice(this.label, this.icon); final String label; final IconData icon; static const speaker = OutputDevice('Phone speaker', Icons.speaker_phone_rounded); /// Android sends media to the most recently connected external device, so /// pick Bluetooth, then wired/USB/car, then the built-in speaker. static OutputDevice from(Set devices) { final outputs = devices.where((d) => d.isOutput).toList(); int rank(AudioDevice d) { final t = d.type.name.toLowerCase(); if (t.contains('bluetooth') && t != 'bluetoothsco') return 0; if (t.contains('car') || t.contains('wired') || t.contains('usb') || t.contains('line')) return 1; if (t.contains('hdmi') || t.contains('airplay') || t.contains('dock')) return 2; if (t == 'builtinspeaker') return 3; return 9; } outputs.sort((a, b) => rank(a).compareTo(rank(b))); if (outputs.isEmpty || rank(outputs.first) >= 3) return speaker; final d = outputs.first; final t = d.type.name.toLowerCase(); final name = d.name.trim().isEmpty ? null : d.name.trim(); if (t.contains('bluetooth')) return OutputDevice(name ?? 'Bluetooth', Icons.bluetooth_audio_rounded); if (t.contains('car')) return OutputDevice(name ?? 'Car audio', Icons.directions_car_rounded); if (t.contains('wired') || t.contains('line')) return const OutputDevice('Headphones', Icons.headphones_rounded); if (t.contains('usb')) return OutputDevice(name ?? 'USB audio', Icons.usb_rounded); return OutputDevice(name ?? 'External speaker', Icons.speaker_rounded); } } final outputDeviceProvider = StreamProvider((ref) async* { final session = await AudioSession.instance; yield OutputDevice.from(await session.getDevices(includeInputs: false)); yield* session.devicesStream.map(OutputDevice.from); }); const _channel = MethodChannel('nk/output'); /// Shows where music is playing; tap to choose another speaker or Bluetooth device. class OutputDeviceButton extends ConsumerWidget { const OutputDeviceButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final device = ref.watch(outputDeviceProvider).value ?? OutputDevice.speaker; final external = device != OutputDevice.speaker; final color = external ? NkColors.teaLeaf : NkColors.mutedOnPhoto; return TextButton.icon( onPressed: () => _choose(context), style: TextButton.styleFrom(foregroundColor: color, visualDensity: VisualDensity.compact), icon: Icon(device.icon, size: 20), label: Text(device.label, maxLines: 1, overflow: TextOverflow.ellipsis), ); } Future _choose(BuildContext context) async { if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { try { if (await _channel.invokeMethod('showOutputSwitcher') == true) return; } on PlatformException { // Fall through to the hint below. } } if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Choose a speaker or Bluetooth device from Control Center (swipe down from the top right).'), )); } }