import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../remote/discovery.dart'; import '../remote/remote_session.dart'; import '../state/remote_providers.dart'; import '../theme/tokens.dart'; /// Open the cross-device control sheet: pick a same-Wi-Fi Timbre device to /// control, or detach back to local playback (updates-features.md #3). Future showDevicesSheet(BuildContext context) { return showModalBottomSheet( context: context, backgroundColor: TimbreColors.background, isScrollControlled: true, builder: (_) => const _DevicesSheet(), ); } class _DevicesSheet extends ConsumerStatefulWidget { const _DevicesSheet(); @override ConsumerState<_DevicesSheet> createState() => _DevicesSheetState(); } class _DevicesSheetState extends ConsumerState<_DevicesSheet> { @override void initState() { super.initState(); // Browse only while the sheet is open — discovery is comparatively costly. Future.microtask( () => ref.read(remoteControlProvider.notifier).startBrowsing()); } @override void dispose() { ref.read(remoteControlProvider.notifier).stopBrowsing(); super.dispose(); } @override Widget build(BuildContext context) { final rc = ref.watch(remoteControlProvider); final accent = Theme.of(context).colorScheme.primary; return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl), child: Text('Devices', style: TextStyle(color: accent, fontWeight: FontWeight.w700)), ), const SizedBox(height: TimbreSpacing.md), if (!rc.supported) const _Note( "Cross-device control isn't available on this platform.") else ...[ // "Play here" row — active when currently controlling a remote. _LocalRow( active: !rc.isAttached, onTap: rc.isAttached ? () => ref.read(remoteControlProvider.notifier).detach() : null, ), const _Divider(), for (final d in rc.devices) _DeviceRow( device: d, active: rc.attachedDevice?.id == d.id, connecting: rc.attachedDevice?.id == d.id && rc.status == RemoteConnStatus.connecting, onTap: () => ref.read(remoteControlProvider.notifier).attach(d), ), if (rc.devices.isEmpty) const _Note('Searching for devices on your Wi-Fi…', spinner: true), if (rc.status == RemoteConnStatus.denied) const _Note( 'That device refused the connection (different account?).'), ], ], ), ), ); } } /// The "play on this device" (local) row, shown selected when not attached. class _LocalRow extends StatelessWidget { const _LocalRow({required this.active, this.onTap}); final bool active; final VoidCallback? onTap; @override Widget build(BuildContext context) { final accent = Theme.of(context).colorScheme.primary; return InkWell( onTap: onTap, child: Container( constraints: const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget), padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl), child: Row( children: [ Icon(Icons.smartphone, size: 16, color: active ? accent : TimbreColors.foreground), const SizedBox(width: TimbreSpacing.md), Expanded( child: Text('This device', style: TextStyle( color: TimbreColors.foreground, fontWeight: active ? FontWeight.w700 : FontWeight.w400, )), ), if (active) Text('●', style: TextStyle(color: accent)), ], ), ), ); } } class _DeviceRow extends StatelessWidget { const _DeviceRow({ required this.device, required this.active, required this.connecting, required this.onTap, }); final DiscoveredDevice device; final bool active; final bool connecting; final VoidCallback onTap; @override Widget build(BuildContext context) { final accent = Theme.of(context).colorScheme.primary; return InkWell( onTap: onTap, child: Container( constraints: const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget), padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl), child: Row( children: [ Icon(Icons.cast, size: 16, color: active ? accent : TimbreColors.foreground), const SizedBox(width: TimbreSpacing.md), Expanded( child: Text( device.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: TimbreColors.foreground, fontWeight: active ? FontWeight.w700 : FontWeight.w400, ), ), ), if (connecting) const SizedBox( height: 14, width: 14, child: CircularProgressIndicator(strokeWidth: 2), ) else if (active) Text('●', style: TextStyle(color: accent)), ], ), ), ); } } class _Divider extends StatelessWidget { const _Divider(); @override Widget build(BuildContext context) => const Padding( padding: EdgeInsets.symmetric( horizontal: TimbreSpacing.xl, vertical: TimbreSpacing.sm), child: Divider(color: TimbreColors.border, height: 1), ); } class _Note extends StatelessWidget { const _Note(this.text, {this.spinner = false}); final String text; final bool spinner; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric( horizontal: TimbreSpacing.xl, vertical: TimbreSpacing.md), child: Row( children: [ if (spinner) ...[ const SizedBox( height: 14, width: 14, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: TimbreSpacing.md), ], Flexible( child: Text(text, style: const TextStyle(color: TimbreColors.dimmed)), ), ], ), ); } }