💫 Animated call overlay

This commit is contained in:
2025-11-23 00:35:42 +08:00
parent 3ad4bb4518
commit a66c6ea654

View File

@@ -1,3 +1,4 @@
import 'package:animations/animations.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -319,29 +320,46 @@ class CallOverlayBar extends HookConsumerWidget {
// Default to true (preview mode) so user sees video immediately after joining // Default to true (preview mode) so user sees video immediately after joining
final isExpanded = useState(true); final isExpanded = useState(true);
// If connected, show active call UI Widget child;
if (callState.isConnected) { if (callState.isConnected) {
return _buildActiveCallOverlay( child = _buildActiveCallOverlay(
context, context,
ref, ref,
callState, callState,
callNotifier, callNotifier,
isExpanded, isExpanded,
); );
} else if (ongoingCall.value != null) {
child = _buildJoinPrompt(context, ref);
} else {
child = const SizedBox.shrink(key: ValueKey('empty'));
} }
// If not connected but there is an ongoing call, show join prompt return AnimatedSize(
if (ongoingCall.value != null) { duration: const Duration(milliseconds: 150),
return _buildJoinPrompt(context, ref); curve: Curves.easeInOut,
} alignment: Alignment.topCenter,
child: AnimatedSwitcher(
return const SizedBox.shrink(); duration: const Duration(milliseconds: 150),
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: child,
),
);
} }
Widget _buildJoinPrompt(BuildContext context, WidgetRef ref) { Widget _buildJoinPrompt(BuildContext context, WidgetRef ref) {
final isLoading = useState(false); final isLoading = useState(false);
return Card( return Card(
key: const ValueKey('join_prompt'),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Row( child: Row(
@@ -441,13 +459,16 @@ class CallOverlayBar extends HookConsumerWidget {
: value, : value,
); );
if (lastSpeaker == null) return const SizedBox.shrink(); if (lastSpeaker == null) {
return const SizedBox.shrink(key: ValueKey('active_waiting'));
}
final userInfo = ref.watch(userInfoProvider).value!; final userInfo = ref.watch(userInfoProvider).value!;
// Preview Mode (Expanded) // Preview Mode (Expanded)
if (isExpanded.value) { if (isExpanded.value) {
return Card( return Card(
key: const ValueKey('active_expanded'),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
@@ -461,20 +482,22 @@ class CallOverlayBar extends HookConsumerWidget {
const Gap(4), const Gap(4),
Text(formatDuration(callState.duration)).bold(), Text(formatDuration(callState.duration)).bold(),
const Spacer(), const Spacer(),
IconButton( OpenContainer(
visualDensity: const VisualDensity( closedElevation: 0,
horizontal: -4, closedColor: Colors.transparent,
vertical: -4, openColor: Theme.of(context).scaffoldBackgroundColor,
), middleColor: Theme.of(context).scaffoldBackgroundColor,
icon: const Icon(Icons.fullscreen), openBuilder: (context, action) => CallScreen(room: room),
onPressed: () { closedBuilder:
Navigator.of(context).push( (context, openContainer) => IconButton(
MaterialPageRoute( visualDensity: const VisualDensity(
builder: (context) => CallScreen(room: room), horizontal: -4,
vertical: -4,
),
icon: const Icon(Icons.fullscreen),
onPressed: openContainer,
tooltip: 'Full Screen',
), ),
);
},
tooltip: 'Full Screen',
), ),
IconButton( IconButton(
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
@@ -504,6 +527,7 @@ class CallOverlayBar extends HookConsumerWidget {
// Compact Mode // Compact Mode
return GestureDetector( return GestureDetector(
key: const ValueKey('active_collapsed'),
onTap: () => isExpanded.value = true, onTap: () => isExpanded.value = true,
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,