💄 Optimize call
This commit is contained in:
@@ -726,5 +726,6 @@
|
||||
"reactionHeart": "Heart",
|
||||
"selectMicrophone": "Select Microphone",
|
||||
"selectCamera": "Select Camera",
|
||||
"switchedTo": "Switched to {}"
|
||||
"switchedTo": "Switched to {}",
|
||||
"connecting": "Connecting"
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
Text(
|
||||
callState.isConnected
|
||||
? formatDuration(callState.duration)
|
||||
: 'Connecting',
|
||||
: 'connecting',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
@@ -61,13 +61,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
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,
|
||||
),
|
||||
SpeakingRippleAvatar(live: live, size: 30),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@@ -131,11 +125,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
for (final live in participants)
|
||||
SpeakingRippleAvatar(
|
||||
isSpeaking: live.isSpeaking,
|
||||
isMuted: live.isMuted,
|
||||
audioLevel:
|
||||
live.remoteParticipant.audioLevel,
|
||||
identity: live.participant.identity,
|
||||
live: live,
|
||||
size: 72,
|
||||
).padding(horizontal: 4),
|
||||
],
|
||||
|
@@ -338,11 +338,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
||||
height: 40,
|
||||
child:
|
||||
SpeakingRippleAvatar(
|
||||
isSpeaking: lastSpeaker.isSpeaking,
|
||||
isMuted: lastSpeaker.isMuted,
|
||||
audioLevel:
|
||||
lastSpeaker.remoteParticipant.audioLevel,
|
||||
identity: lastSpeaker.participant.identity,
|
||||
live: lastSpeaker,
|
||||
size: 36,
|
||||
).center(),
|
||||
);
|
||||
|
94
lib/widgets/chat/call_participant_card.dart
Normal file
94
lib/widgets/chat/call_participant_card.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_popup_card/flutter_popup_card.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/widgets/account/account_nameplate.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class CallParticipantCard extends HookConsumerWidget {
|
||||
final CallParticipantLive live;
|
||||
const CallParticipantCard({super.key, required this.live});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final width =
|
||||
math.min(MediaQuery.of(context).size.width - 80, 360).toDouble();
|
||||
return PopupCard(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.wifi, size: 16),
|
||||
const Gap(8),
|
||||
Text(switch (live.remoteParticipant.connectionQuality) {
|
||||
ConnectionQuality.excellent => 'Excellent',
|
||||
ConnectionQuality.good => 'Good',
|
||||
ConnectionQuality.poor => 'Bad',
|
||||
ConnectionQuality.lost => 'Lost',
|
||||
_ => 'Connecting',
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16),
|
||||
AccountNameplate(
|
||||
name: live.participant.identity,
|
||||
isOutlined: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CallParticipantGestureDetector extends StatelessWidget {
|
||||
final CallParticipantLive participant;
|
||||
final Widget child;
|
||||
const CallParticipantGestureDetector({
|
||||
super.key,
|
||||
required this.participant,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: child,
|
||||
onTapDown: (details) {
|
||||
showCallParticipantCard(
|
||||
context,
|
||||
participant,
|
||||
offset: details.localPosition,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showCallParticipantCard(
|
||||
BuildContext context,
|
||||
CallParticipantLive participant, {
|
||||
Offset? offset,
|
||||
}) async {
|
||||
await showPopupCard<void>(
|
||||
offset: offset ?? Offset.zero,
|
||||
context: context,
|
||||
builder: (context) => CallParticipantCard(live: participant),
|
||||
alignment: Alignment.center,
|
||||
dimBackground: true,
|
||||
);
|
||||
}
|
@@ -2,34 +2,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/widgets/account/account_pfc.dart';
|
||||
import 'package:island/widgets/chat/call_participant_card.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
final bool isSpeaking;
|
||||
final bool isMuted;
|
||||
final double audioLevel;
|
||||
final String identity;
|
||||
final CallParticipantLive live;
|
||||
final double size;
|
||||
|
||||
const SpeakingRippleAvatar({
|
||||
super.key,
|
||||
required this.isSpeaking,
|
||||
required this.isMuted,
|
||||
required this.audioLevel,
|
||||
required this.identity,
|
||||
this.size = 96,
|
||||
});
|
||||
const SpeakingRippleAvatar({super.key, required this.live, this.size = 96});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final account = ref.watch(accountProvider(identity));
|
||||
final account = ref.watch(accountProvider(live.participant.identity));
|
||||
|
||||
final avatarRadius = size / 2;
|
||||
final clampedLevel = audioLevel.clamp(0.0, 1.0);
|
||||
final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0);
|
||||
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
||||
return SizedBox(
|
||||
width: size + 8,
|
||||
@@ -37,7 +27,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: avatarRadius,
|
||||
end: isSpeaking ? rippleRadius : avatarRadius,
|
||||
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
||||
),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
@@ -45,7 +35,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (isSpeaking)
|
||||
if (live.remoteParticipant.isSpeaking)
|
||||
Container(
|
||||
width: animatedRadius * 2,
|
||||
height: animatedRadius * 2,
|
||||
@@ -61,8 +51,8 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: account.when(
|
||||
data:
|
||||
(value) => AccountPfcGestureDetector(
|
||||
uname: identity,
|
||||
(value) => CallParticipantGestureDetector(
|
||||
participant: live,
|
||||
child: ProfilePictureWidget(
|
||||
file: value.profile.picture,
|
||||
radius: size / 2,
|
||||
@@ -80,7 +70,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isMuted)
|
||||
if (live.remoteParticipant.isMuted)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
@@ -118,7 +108,6 @@ class CallParticipantTile extends HookConsumerWidget {
|
||||
live.remoteParticipant.trackPublications.values
|
||||
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
|
||||
.isNotEmpty;
|
||||
final audioLevel = live.remoteParticipant.audioLevel;
|
||||
|
||||
if (hasVideo) {
|
||||
return Stack(
|
||||
@@ -159,13 +148,7 @@ class CallParticipantTile extends HookConsumerWidget {
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SpeakingRippleAvatar(
|
||||
isSpeaking: live.isSpeaking,
|
||||
isMuted: live.isMuted,
|
||||
audioLevel: audioLevel,
|
||||
identity: live.participant.identity,
|
||||
size: 84,
|
||||
);
|
||||
return SpeakingRippleAvatar(size: 84, live: live);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user