196 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart' hide ConnectionState;
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/pods/chat/call.dart';
 | 
						|
import 'package:island/talker.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:island/widgets/alert.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(() {
 | 
						|
      talker.info('[Call] Joining the call...');
 | 
						|
      callNotifier.joinRoom(roomId).catchError((_) {
 | 
						|
        showConfirmAlert(
 | 
						|
          'Seems there already has a call connected, do you want override it?',
 | 
						|
          'Call already connected',
 | 
						|
        ).then((value) {
 | 
						|
          if (value != true) return;
 | 
						|
          talker.info('[Call] Joining the call... with overrides');
 | 
						|
          callNotifier.disconnect();
 | 
						|
          callNotifier.dispose();
 | 
						|
          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 &&
 | 
						|
                    !pub.muted &&
 | 
						|
                    !pub.isDisposed,
 | 
						|
              )),
 | 
						|
    );
 | 
						|
 | 
						|
    return AppScaffold(
 | 
						|
      isNoBackground: 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)
 | 
						|
                  : (switch (callNotifier.room?.connectionState) {
 | 
						|
                    ConnectionState.connected => 'connected',
 | 
						|
                    ConnectionState.connecting => 'connecting',
 | 
						|
                    ConnectionState.reconnecting => 'reconnecting',
 | 
						|
                    _ => 'disconnected',
 | 
						|
                  }).tr(),
 | 
						|
              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),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |