💄 Fixes of call and optimizations
This commit is contained in:
@@ -4,7 +4,6 @@ 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/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:island/widgets/chat/call_overlay.dart';
|
||||
@@ -21,14 +20,20 @@ class CallScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
callNotifier.joinRoom(roomId);
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
final viewMode = useState<String>('grid');
|
||||
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,
|
||||
@@ -50,39 +55,50 @@ class CallScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Symbols.grid_view),
|
||||
tooltip: 'Grid View',
|
||||
onPressed: () => viewMode.value = 'grid',
|
||||
color:
|
||||
viewMode.value == 'grid'
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
if (!allAudioOnly)
|
||||
SingleChildScrollView(
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
for (final live in callNotifier.participants)
|
||||
SpeakingRippleAvatar(
|
||||
isSpeaking: live.isSpeaking,
|
||||
isMuted: live.isMuted,
|
||||
audioLevel: live.remoteParticipant.audioLevel,
|
||||
identity: live.participant.identity,
|
||||
size: 30,
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Symbols.view_agenda),
|
||||
tooltip: 'Stage View',
|
||||
onPressed: () => viewMode.value = 'stage',
|
||||
color:
|
||||
viewMode.value == 'stage'
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
body:
|
||||
callState.error != null
|
||||
? Center(
|
||||
child: Text(
|
||||
callState.error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
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(
|
||||
@@ -100,17 +116,8 @@ class CallScreen extends HookConsumerWidget {
|
||||
child: Text('No participants in call'),
|
||||
);
|
||||
}
|
||||
|
||||
final participants = callNotifier.participants;
|
||||
final allAudioOnly = participants.every(
|
||||
(p) =>
|
||||
!(p.hasVideo &&
|
||||
p.remoteParticipant.trackPublications.values
|
||||
.any(
|
||||
(pub) =>
|
||||
pub.track != null &&
|
||||
pub.kind == TrackType.VIDEO,
|
||||
)),
|
||||
);
|
||||
if (allAudioOnly) {
|
||||
// Audio-only: show avatars in a compact row
|
||||
return Center(
|
||||
@@ -123,138 +130,45 @@ class CallScreen extends HookConsumerWidget {
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final live in participants)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: SpeakingRippleAvatar(
|
||||
isSpeaking: live.isSpeaking,
|
||||
audioLevel:
|
||||
live.remoteParticipant.audioLevel,
|
||||
pictureId:
|
||||
live
|
||||
.participant
|
||||
.profile
|
||||
?.account
|
||||
.profile
|
||||
.picture
|
||||
?.id,
|
||||
size: 72,
|
||||
),
|
||||
),
|
||||
SpeakingRippleAvatar(
|
||||
isSpeaking: live.isSpeaking,
|
||||
isMuted: live.isMuted,
|
||||
audioLevel:
|
||||
live.remoteParticipant.audioLevel,
|
||||
identity: live.participant.identity,
|
||||
size: 72,
|
||||
).padding(horizontal: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (viewMode.value == 'stage') {
|
||||
// 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);
|
||||
}
|
||||
final others =
|
||||
participants
|
||||
.where((p) => !mainSpeakers.contains(p))
|
||||
.toList();
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (final speaker in mainSpeakers)
|
||||
Expanded(
|
||||
child:
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
child: Column(
|
||||
children: [
|
||||
CallParticipantTile(
|
||||
live: speaker,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).center(),
|
||||
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
),
|
||||
if (others.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (final other in others)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: CallParticipantTile(
|
||||
live: other,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
)
|
||||
.toList();
|
||||
if (mainSpeakers.isEmpty && participants.isNotEmpty) {
|
||||
mainSpeakers.add(participants.first);
|
||||
}
|
||||
// Default: grid view
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount:
|
||||
isWidestScreen(context)
|
||||
? 4
|
||||
: isWiderScreen(context)
|
||||
? 3
|
||||
: 2,
|
||||
childAspectRatio: 16 / 9,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
return Column(
|
||||
children: [
|
||||
for (final speaker in mainSpeakers)
|
||||
Expanded(
|
||||
child: CallParticipantTile(live: speaker),
|
||||
),
|
||||
itemCount: participants.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final live = participants[idx];
|
||||
return AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
children: [CallParticipantTile(live: live)],
|
||||
),
|
||||
),
|
||||
),
|
||||
).center();
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
Reference in New Issue
Block a user