💄 Optimize call
This commit is contained in:
@@ -726,5 +726,6 @@
|
|||||||
"reactionHeart": "Heart",
|
"reactionHeart": "Heart",
|
||||||
"selectMicrophone": "Select Microphone",
|
"selectMicrophone": "Select Microphone",
|
||||||
"selectCamera": "Select Camera",
|
"selectCamera": "Select Camera",
|
||||||
"switchedTo": "Switched to {}"
|
"switchedTo": "Switched to {}",
|
||||||
|
"connecting": "Connecting"
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
callState.isConnected
|
callState.isConnected
|
||||||
? formatDuration(callState.duration)
|
? formatDuration(callState.duration)
|
||||||
: 'Connecting',
|
: 'connecting',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -61,13 +61,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
for (final live in callNotifier.participants)
|
for (final live in callNotifier.participants)
|
||||||
SpeakingRippleAvatar(
|
SpeakingRippleAvatar(live: live, size: 30),
|
||||||
isSpeaking: live.isSpeaking,
|
|
||||||
isMuted: live.isMuted,
|
|
||||||
audioLevel: live.remoteParticipant.audioLevel,
|
|
||||||
identity: live.participant.identity,
|
|
||||||
size: 30,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -131,11 +125,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
for (final live in participants)
|
for (final live in participants)
|
||||||
SpeakingRippleAvatar(
|
SpeakingRippleAvatar(
|
||||||
isSpeaking: live.isSpeaking,
|
live: live,
|
||||||
isMuted: live.isMuted,
|
|
||||||
audioLevel:
|
|
||||||
live.remoteParticipant.audioLevel,
|
|
||||||
identity: live.participant.identity,
|
|
||||||
size: 72,
|
size: 72,
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
],
|
],
|
||||||
|
@@ -338,11 +338,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child:
|
child:
|
||||||
SpeakingRippleAvatar(
|
SpeakingRippleAvatar(
|
||||||
isSpeaking: lastSpeaker.isSpeaking,
|
live: lastSpeaker,
|
||||||
isMuted: lastSpeaker.isMuted,
|
|
||||||
audioLevel:
|
|
||||||
lastSpeaker.remoteParticipant.audioLevel,
|
|
||||||
identity: lastSpeaker.participant.identity,
|
|
||||||
size: 36,
|
size: 36,
|
||||||
).center(),
|
).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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/call.dart';
|
import 'package:island/pods/call.dart';
|
||||||
import 'package:island/screens/account/profile.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:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class SpeakingRippleAvatar extends HookConsumerWidget {
|
class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||||
final bool isSpeaking;
|
final CallParticipantLive live;
|
||||||
final bool isMuted;
|
|
||||||
final double audioLevel;
|
|
||||||
final String identity;
|
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
const SpeakingRippleAvatar({
|
const SpeakingRippleAvatar({super.key, required this.live, this.size = 96});
|
||||||
super.key,
|
|
||||||
required this.isSpeaking,
|
|
||||||
required this.isMuted,
|
|
||||||
required this.audioLevel,
|
|
||||||
required this.identity,
|
|
||||||
this.size = 96,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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 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);
|
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: size + 8,
|
width: size + 8,
|
||||||
@@ -37,7 +27,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
child: TweenAnimationBuilder<double>(
|
child: TweenAnimationBuilder<double>(
|
||||||
tween: Tween<double>(
|
tween: Tween<double>(
|
||||||
begin: avatarRadius,
|
begin: avatarRadius,
|
||||||
end: isSpeaking ? rippleRadius : avatarRadius,
|
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
||||||
),
|
),
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
@@ -45,7 +35,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isSpeaking)
|
if (live.remoteParticipant.isSpeaking)
|
||||||
Container(
|
Container(
|
||||||
width: animatedRadius * 2,
|
width: animatedRadius * 2,
|
||||||
height: animatedRadius * 2,
|
height: animatedRadius * 2,
|
||||||
@@ -61,8 +51,8 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||||
child: account.when(
|
child: account.when(
|
||||||
data:
|
data:
|
||||||
(value) => AccountPfcGestureDetector(
|
(value) => CallParticipantGestureDetector(
|
||||||
uname: identity,
|
participant: live,
|
||||||
child: ProfilePictureWidget(
|
child: ProfilePictureWidget(
|
||||||
file: value.profile.picture,
|
file: value.profile.picture,
|
||||||
radius: size / 2,
|
radius: size / 2,
|
||||||
@@ -80,7 +70,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isMuted)
|
if (live.remoteParticipant.isMuted)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
@@ -118,7 +108,6 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
live.remoteParticipant.trackPublications.values
|
live.remoteParticipant.trackPublications.values
|
||||||
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
|
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
final audioLevel = live.remoteParticipant.audioLevel;
|
|
||||||
|
|
||||||
if (hasVideo) {
|
if (hasVideo) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -159,13 +148,7 @@ class CallParticipantTile extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SpeakingRippleAvatar(
|
return SpeakingRippleAvatar(size: 84, live: live);
|
||||||
isSpeaking: live.isSpeaking,
|
|
||||||
isMuted: live.isMuted,
|
|
||||||
audioLevel: audioLevel,
|
|
||||||
identity: live.participant.identity,
|
|
||||||
size: 84,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user