import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/call.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/chat/call_button.dart'; import 'package:island/widgets/chat/call_overlay.dart'; import 'package:island/widgets/chat/call_participant_tile.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; class CallScreen extends HookConsumerWidget { final String roomId; const CallScreen({super.key, required this.roomId}); @override Widget build(BuildContext context, WidgetRef ref) { final ongoingCall = ref.watch(ongoingCallProvider(roomId)); final callState = ref.watch(callNotifierProvider); final callNotifier = ref.watch(callNotifierProvider.notifier); useEffect(() { callNotifier.joinRoom(roomId); return null; }, []); final allAudioOnly = callNotifier.participants.every( (p) => !(p.hasVideo && p.remoteParticipant.trackPublications.values.any( (pub) => pub.track != null && pub.kind == TrackType.VIDEO, )), ); return AppScaffold( noBackground: false, appBar: AppBar( leading: PageBackButton(), title: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( ongoingCall.value?.room.name ?? 'call'.tr(), style: const TextStyle(fontSize: 16), ), Text( callState.isConnected ? formatDuration(callState.duration) : 'connecting', style: const TextStyle(fontSize: 14), ), ], ), actions: [ if (!allAudioOnly) SingleChildScrollView( child: Row( spacing: 4, children: [ for (final live in callNotifier.participants) SpeakingRippleAvatar(live: live, size: 30), const Gap(8), ], ), ), ], ), body: callState.error != null ? Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 320), child: Column( children: [ const Icon(Symbols.error_outline, size: 48), const Gap(4), Text( callState.error!, textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF757575)), ), const Gap(8), TextButton( onPressed: () { callNotifier.disconnect(); callNotifier.dispose(); callNotifier.joinRoom(roomId); }, child: Text('retry').tr(), ), ], ), ), ) : Column( children: [ Expanded( child: Builder( builder: (context) { if (!callState.isConnected) { return const Center( child: CircularProgressIndicator(), ); } if (callNotifier.participants.isEmpty) { return const Center( child: Text('No participants in call'), ); } final participants = callNotifier.participants; if (allAudioOnly) { // Audio-only: show avatars in a compact row return Center( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center, spacing: 8, runSpacing: 8, children: [ for (final live in participants) SpeakingRippleAvatar( live: live, size: 72, ).padding(horizontal: 4), ], ), ), ); } // Stage view: show main speaker(s) large, others in row final mainSpeakers = participants .where( (p) => p .remoteParticipant .trackPublications .values .any( (pub) => pub.track != null && pub.kind == TrackType.VIDEO, ), ) .toList(); if (mainSpeakers.isEmpty && participants.isNotEmpty) { mainSpeakers.add(participants.first); } return Column( children: [ for (final speaker in mainSpeakers) Expanded( child: CallParticipantTile(live: speaker), ), ], ); }, ), ), CallControlsBar(), Gap(MediaQuery.of(context).padding.bottom + 16), ], ), ); } }