import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/chat/call.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/chat/call_participant_tile.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; class CallControlsBar extends HookConsumerWidget { const CallControlsBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final callState = ref.watch(callNotifierProvider); final callNotifier = ref.read(callNotifierProvider.notifier); return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Wrap( alignment: WrapAlignment.center, runSpacing: 16, spacing: 16, children: [ _buildCircularButtonWithDropdown( context: context, ref: ref, icon: callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off, onPressed: () => callNotifier.toggleCamera(), backgroundColor: const Color(0xFF424242), hasDropdown: true, deviceType: 'videoinput', ), _buildCircularButton( icon: callState.isScreenSharing ? Icons.stop_screen_share : Icons.screen_share, onPressed: () => callNotifier.toggleScreenShare(context), backgroundColor: const Color(0xFF424242), ), _buildCircularButtonWithDropdown( context: context, ref: ref, icon: callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off, onPressed: () => callNotifier.toggleMicrophone(), backgroundColor: const Color(0xFF424242), hasDropdown: true, deviceType: 'audioinput', ), _buildCircularButton( icon: callState.isSpeakerphone ? Symbols.mobile_speaker : Symbols.ear_sound, onPressed: () => callNotifier.toggleSpeakerphone(), backgroundColor: const Color(0xFF424242), ), _buildCircularButton( icon: Icons.call_end, onPressed: () => showModalBottomSheet( context: context, isScrollControlled: true, useRootNavigator: true, builder: (innerContext) => Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Symbols.logout, fill: 1), title: Text('callLeave').tr(), onTap: () { callNotifier.disconnect(); Navigator.of(context).pop(); Navigator.of(innerContext).pop(); }, ), ListTile( leading: const Icon(Symbols.call_end, fill: 1), iconColor: Colors.red, title: Text('callEnd').tr(), onTap: () async { callNotifier.disconnect(); final apiClient = ref.watch(apiClientProvider); try { showLoadingModal(context); await apiClient.delete( '/sphere/chat/realtime/${callNotifier.roomId}', ); callNotifier.dispose(); if (context.mounted) { Navigator.of(context).pop(); Navigator.of(innerContext).pop(); } } catch (err) { showErrorAlert(err); } finally { if (context.mounted) hideLoadingModal(context); } }, ), Gap(MediaQuery.of(context).padding.bottom), ], ), ), backgroundColor: const Color(0xFFE53E3E), iconColor: Colors.white, ), ], ), ); } Widget _buildCircularButton({ required IconData icon, required VoidCallback onPressed, required Color backgroundColor, Color? iconColor, }) { return Container( width: 56, height: 56, decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), child: IconButton( icon: Icon(icon, color: iconColor ?? Colors.white, size: 24), onPressed: onPressed, ), ); } Widget _buildCircularButtonWithDropdown({ required BuildContext context, required WidgetRef ref, required IconData icon, required VoidCallback onPressed, required Color backgroundColor, required bool hasDropdown, Color? iconColor, String? deviceType, // 'videoinput' or 'audioinput' }) { return Stack( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: backgroundColor, shape: BoxShape.circle, ), child: IconButton( icon: Icon(icon, color: iconColor ?? Colors.white, size: 24), onPressed: onPressed, ), ), if (hasDropdown && deviceType != null) Positioned( bottom: 4, right: 4, child: GestureDetector( onTap: () => _showDeviceSelectionDialog(context, ref, deviceType), child: Container( width: 16, height: 16, decoration: BoxDecoration( color: backgroundColor.withOpacity(0.8), shape: BoxShape.circle, border: Border.all( color: Colors.white.withOpacity(0.3), width: 0.5, ), ), child: Icon( Icons.arrow_drop_down, color: Colors.white, size: 12, ), ), ), ), ], ); } Future _showDeviceSelectionDialog( BuildContext context, WidgetRef ref, String deviceType, ) async { try { final devices = await navigator.mediaDevices.enumerateDevices(); final filteredDevices = devices.where((device) { if (deviceType == 'videoinput') { return device.kind == 'videoinput'; } else if (deviceType == 'audioinput') { return device.kind == 'audioinput'; } return false; }).toList(); if (!context.mounted) return; showModalBottomSheet( context: context, builder: (BuildContext dialogContext) { return SheetScaffold( titleText: deviceType == 'videoinput' ? 'selectCamera'.tr() : 'selectMicrophone'.tr(), child: ListView.builder( itemCount: filteredDevices.length, itemBuilder: (context, index) { final device = filteredDevices[index]; return ListTile( title: Text( device.label.isNotEmpty ? device.label : '${'device'.tr()} ${index + 1}', ), onTap: () { Navigator.of(dialogContext).pop(); _switchDevice(context, ref, device, deviceType); }, ); }, ), ); }, ); } catch (e) { showErrorAlert(e); } } Future _switchDevice( BuildContext context, WidgetRef ref, MediaDeviceInfo device, String deviceType, ) async { try { final callNotifier = ref.read(callNotifierProvider.notifier); if (callNotifier.webrtcManager == null) return; if (deviceType == 'videoinput') { await callNotifier.webrtcManager!.switchCamera(device.deviceId); } else if (deviceType == 'audioinput') { await callNotifier.webrtcManager!.switchMicrophone(device.deviceId); } if (context.mounted) { showSnackBar( 'switchedTo'.tr( args: [device.label.isNotEmpty ? device.label : 'device'], ), ); } } catch (err) { showErrorAlert(err); } } } class CallOverlayBar extends HookConsumerWidget { const CallOverlayBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final callState = ref.watch(callNotifierProvider); final callNotifier = ref.read(callNotifierProvider.notifier); // Only show if connected and not on the call screen if (!callState.isConnected) return const SizedBox.shrink(); final lastSpeaker = callNotifier.participants.isNotEmpty ? callNotifier.participants.first : null; final actionButtonStyle = ButtonStyle( minimumSize: const MaterialStatePropertyAll(Size(24, 24)), ); return GestureDetector( child: Card( margin: EdgeInsets.zero, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Row( children: [ Builder( builder: (context) { if (lastSpeaker == null) { return const CircularProgressIndicator(); } return SizedBox( width: 40, height: 40, child: SpeakingRippleAvatar( live: lastSpeaker, size: 36, ), ); }, ), const Gap(8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '@${lastSpeaker?.participant.identity ?? 'Unknown'}', ), Text( formatDuration(callState.duration), style: Theme.of(context).textTheme.bodySmall, ), ], ), ], ), ), IconButton( icon: Icon( callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off, ), onPressed: () { callNotifier.toggleMicrophone(); }, style: actionButtonStyle, ), IconButton( icon: Icon( callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off, ), onPressed: () { callNotifier.toggleCamera(); }, style: actionButtonStyle, ), IconButton( icon: Icon( callState.isScreenSharing ? Icons.stop_screen_share : Icons.screen_share, ), onPressed: () { callNotifier.toggleScreenShare(context); }, style: actionButtonStyle, ), ], ).padding(all: 16), ), onTap: () { context.pushNamed( 'chatCall', pathParameters: {'id': callNotifier.roomId!}, ); }, ); } }