♻️ Rebuild the call
This commit is contained in:
@@ -1342,5 +1342,6 @@
|
|||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"clearCompleted": "Clear Completed",
|
"clearCompleted": "Clear Completed",
|
||||||
"contentCantEmpty": "Content cannot be empty",
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
"features": "Features"
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed"
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,11 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
String? _roomId;
|
String? _roomId;
|
||||||
String? get roomId => _roomId;
|
String? get roomId => _roomId;
|
||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
SnChatRoom? _chatRoom;
|
||||||
|
SnChatRoom? get chatRoom => _chatRoom;
|
||||||
|
|
||||||
|
Future<void> joinRoom(SnChatRoom room) async {
|
||||||
|
var roomId = room.id;
|
||||||
if (_roomId == roomId && _room != null) {
|
if (_roomId == roomId && _room != null) {
|
||||||
talker.info('[Call] Call skipped. Already has data');
|
talker.info('[Call] Call skipped. Already has data');
|
||||||
return;
|
return;
|
||||||
@@ -223,6 +227,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
_chatRoom = room;
|
||||||
if (_room != null) {
|
if (_room != null) {
|
||||||
await _room!.disconnect();
|
await _room!.disconnect();
|
||||||
await _room!.dispose();
|
await _room!.dispose();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
String _$callNotifierHash() => r'2caee30f42315e539cb4df17c0d464ceed41ffa0';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class _SiteFilesProviderElement
|
|||||||
String? get path => (origin as SiteFilesProvider).path;
|
String? get path => (origin as SiteFilesProvider).path;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$siteFileContentHash() => r'bb820f0fe5bdca55efb08beee97aa38d09be04a7';
|
String _$siteFileContentHash() => r'b594ad4f8c54555e742ece94ee001092cb2f83d1';
|
||||||
|
|
||||||
/// See also [siteFileContent].
|
/// See also [siteFileContent].
|
||||||
@ProviderFor(siteFileContent)
|
@ProviderFor(siteFileContent)
|
||||||
@@ -300,5 +300,152 @@ class _SiteFileContentProviderElement
|
|||||||
String get relativePath => (origin as SiteFileContentProvider).relativePath;
|
String get relativePath => (origin as SiteFileContentProvider).relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _$siteFileContentRawHash() =>
|
||||||
|
r'd0331c30698a9f4b90fe9b79273ff5914fa46616';
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
@ProviderFor(siteFileContentRaw)
|
||||||
|
const siteFileContentRawProvider = SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawFamily extends Family<AsyncValue<String>> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
const SiteFileContentRawFamily();
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider call({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) {
|
||||||
|
return SiteFileContentRawProvider(
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SiteFileContentRawProvider getProviderOverride(
|
||||||
|
covariant SiteFileContentRawProvider provider,
|
||||||
|
) {
|
||||||
|
return call(siteId: provider.siteId, relativePath: provider.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'siteFileContentRawProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
class SiteFileContentRawProvider extends AutoDisposeFutureProvider<String> {
|
||||||
|
/// See also [siteFileContentRaw].
|
||||||
|
SiteFileContentRawProvider({
|
||||||
|
required String siteId,
|
||||||
|
required String relativePath,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => siteFileContentRaw(
|
||||||
|
ref as SiteFileContentRawRef,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
from: siteFileContentRawProvider,
|
||||||
|
name: r'siteFileContentRawProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$siteFileContentRawHash,
|
||||||
|
dependencies: SiteFileContentRawFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SiteFileContentRawFamily._allTransitiveDependencies,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
SiteFileContentRawProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.siteId,
|
||||||
|
required this.relativePath,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String siteId;
|
||||||
|
final String relativePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<String> Function(SiteFileContentRawRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SiteFileContentRawProvider._internal(
|
||||||
|
(ref) => create(ref as SiteFileContentRawRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
siteId: siteId,
|
||||||
|
relativePath: relativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<String> createElement() {
|
||||||
|
return _SiteFileContentRawProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SiteFileContentRawProvider &&
|
||||||
|
other.siteId == siteId &&
|
||||||
|
other.relativePath == relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, siteId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, relativePath.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SiteFileContentRawRef on AutoDisposeFutureProviderRef<String> {
|
||||||
|
/// The parameter `siteId` of this provider.
|
||||||
|
String get siteId;
|
||||||
|
|
||||||
|
/// The parameter `relativePath` of this provider.
|
||||||
|
String get relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SiteFileContentRawProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<String>
|
||||||
|
with SiteFileContentRawRef {
|
||||||
|
_SiteFileContentRawProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get siteId => (origin as SiteFileContentRawProvider).siteId;
|
||||||
|
@override
|
||||||
|
String get relativePath =>
|
||||||
|
(origin as SiteFileContentRawProvider).relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import 'package:island/screens/account/me/account_settings.dart';
|
|||||||
import 'package:island/screens/chat/chat.dart';
|
import 'package:island/screens/chat/chat.dart';
|
||||||
import 'package:island/screens/chat/room.dart';
|
import 'package:island/screens/chat/room.dart';
|
||||||
import 'package:island/screens/chat/room_detail.dart';
|
import 'package:island/screens/chat/room_detail.dart';
|
||||||
import 'package:island/screens/chat/call.dart';
|
|
||||||
import 'package:island/screens/chat/search_messages.dart';
|
import 'package:island/screens/chat/search_messages.dart';
|
||||||
import 'package:island/screens/thought/think.dart';
|
import 'package:island/screens/thought/think.dart';
|
||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
@@ -119,14 +118,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ArticleEditScreen(id: id);
|
return ArticleEditScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'chatCall',
|
|
||||||
path: '/chat/:id/call',
|
|
||||||
builder: (context, state) {
|
|
||||||
final id = state.pathParameters['id']!;
|
|
||||||
return CallScreen(roomId: id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
|
|||||||
@@ -3,30 +3,31 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.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';
|
|
||||||
|
|
||||||
class CallScreen extends HookConsumerWidget {
|
class CallScreen extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const CallScreen({super.key, required this.roomId});
|
const CallScreen({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
talker.info('[Call] Joining the call...');
|
talker.info('[Call] Joining the call...');
|
||||||
callNotifier.joinRoom(roomId).catchError((_) {
|
callNotifier.joinRoom(room).catchError((_) {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
'Seems there already has a call connected, do you want override it?',
|
'Seems there already has a call connected, do you want override it?',
|
||||||
'Call already connected',
|
'Call already connected',
|
||||||
@@ -35,7 +36,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
talker.info('[Call] Joining the call... with overrides');
|
talker.info('[Call] Joining the call... with overrides');
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -110,7 +111,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
callNotifier.joinRoom(roomId);
|
callNotifier.joinRoom(room);
|
||||||
},
|
},
|
||||||
child: Text('retry').tr(),
|
child: Text('retry').tr(),
|
||||||
),
|
),
|
||||||
@@ -120,72 +121,7 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: CallContent()),
|
||||||
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(),
|
CallControlsBar(),
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -738,7 +738,11 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
AudioCallButton(roomId: id),
|
chatRoom.when(
|
||||||
|
data: (data) => AudioCallButton(room: data!),
|
||||||
|
error: (err, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -839,7 +843,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
child: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(data) => CallOverlayBar(
|
||||||
|
room: data!,
|
||||||
|
).padding(horizontal: 8, top: 12),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isSyncing)
|
if (isSyncing)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class MessageItemWrapper extends HookConsumerWidget {
|
|||||||
skipError: true,
|
skipError: true,
|
||||||
data: (identity) => _buildContent(context, identity),
|
data: (identity) => _buildContent(context, identity),
|
||||||
loading: () => _buildLoading(),
|
loading: () => _buildLoading(),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!shouldAnimate) {
|
if (!shouldAnimate) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
@@ -28,12 +28,12 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AudioCallButton extends HookConsumerWidget {
|
class AudioCallButton extends HookConsumerWidget {
|
||||||
final String roomId;
|
final SnChatRoom room;
|
||||||
const AudioCallButton({super.key, required this.roomId});
|
const AudioCallButton({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
final isLoading = useState(false);
|
final isLoading = useState(false);
|
||||||
@@ -42,10 +42,9 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleJoin() async {
|
Future<void> handleJoin() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/sphere/chat/realtime/$roomId');
|
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
||||||
if (context.mounted) {
|
// Just join the room, the overlay will handle the UI
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
await callNotifier.joinRoom(room);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -56,7 +55,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
Future<void> handleEnd() async {
|
Future<void> handleEnd() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.delete('/sphere/chat/realtime/$roomId');
|
await apiClient.delete('/sphere/chat/realtime/${room.id}');
|
||||||
callNotifier.dispose(); // Clean up call resources
|
callNotifier.dispose(); // Clean up call resources
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
@@ -94,9 +93,14 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Join Ongoing Call',
|
tooltip: 'Join Ongoing Call',
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (context.mounted) {
|
isLoading.value = true;
|
||||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
try {
|
||||||
|
await callNotifier.joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -105,7 +109,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
// Show join/start call button
|
// Show join/start call button
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.call),
|
icon: const Icon(Icons.call),
|
||||||
tooltip: 'Start/Join Call',
|
tooltip: 'Start Call',
|
||||||
onPressed: handleJoin,
|
onPressed: handleJoin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
77
lib/widgets/chat/call_content.dart
Normal file
77
lib/widgets/chat/call_content.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class CallContent extends HookConsumerWidget {
|
||||||
|
const CallContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final callState = ref.watch(callNotifierProvider);
|
||||||
|
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||||
|
|
||||||
|
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;
|
||||||
|
final allAudioOnly = participants.every(
|
||||||
|
(p) =>
|
||||||
|
!(p.hasVideo &&
|
||||||
|
p.remoteParticipant.trackPublications.values.any(
|
||||||
|
(pub) =>
|
||||||
|
pub.track != null &&
|
||||||
|
pub.kind == TrackType.VIDEO &&
|
||||||
|
!pub.muted &&
|
||||||
|
!pub.isDisposed,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
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:go_router/go_router.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
|
import 'package:island/screens/chat/call.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_content.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -13,7 +19,8 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
class CallControlsBar extends HookConsumerWidget {
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
const CallControlsBar({super.key});
|
final bool isCompact;
|
||||||
|
const CallControlsBar({super.key, this.isCompact = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -21,11 +28,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isCompact ? 12 : 20,
|
||||||
|
vertical: isCompact ? 8 : 16,
|
||||||
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
runSpacing: 16,
|
runSpacing: isCompact ? 12 : 16,
|
||||||
spacing: 16,
|
spacing: isCompact ? 12 : 16,
|
||||||
children: [
|
children: [
|
||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -73,12 +83,15 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
(innerContext) => Column(
|
(innerContext) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
const Gap(24),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.logout, fill: 1),
|
leading: const Icon(Symbols.logout, fill: 1),
|
||||||
title: Text('callLeave').tr(),
|
title: Text('callLeave').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
callNotifier.disconnect();
|
callNotifier.disconnect();
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -96,7 +109,9 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
callNotifier.dispose();
|
callNotifier.dispose();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
Navigator.of(innerContext).pop();
|
Navigator.of(innerContext).pop();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -124,12 +139,14 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
required Color backgroundColor,
|
required Color backgroundColor,
|
||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Container(
|
return Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -145,29 +162,38 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
String? deviceType, // 'videoinput' or 'audioinput'
|
String? deviceType, // 'videoinput' or 'audioinput'
|
||||||
}) {
|
}) {
|
||||||
|
final size = isCompact ? 40.0 : 56.0;
|
||||||
|
final iconSize = isCompact ? 20.0 : 24.0;
|
||||||
return Stack(
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: size,
|
||||||
height: 56,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasDropdown && deviceType != null)
|
if (hasDropdown && deviceType != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 0,
|
||||||
right: 4,
|
right: isCompact ? 0 : -4,
|
||||||
child: GestureDetector(
|
child: Material(
|
||||||
onTap: () => _showDeviceSelectionDialog(context, ref, deviceType),
|
color:
|
||||||
|
Colors
|
||||||
|
.transparent, // Make Material transparent to show underlying color
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
() => _showDeviceSelectionDialog(context, ref, deviceType),
|
||||||
|
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 16,
|
width: isCompact ? 16 : 24,
|
||||||
height: 16,
|
height: isCompact ? 16 : 24,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor.withOpacity(0.8),
|
color: backgroundColor.withOpacity(0.8),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
@@ -179,7 +205,8 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 12,
|
size: isCompact ? 12 : 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -279,34 +306,133 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CallOverlayBar extends HookConsumerWidget {
|
class CallOverlayBar extends HookConsumerWidget {
|
||||||
const CallOverlayBar({super.key});
|
final SnChatRoom room;
|
||||||
|
const CallOverlayBar({super.key, required this.room});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
// Only show if connected and not on the call screen
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
if (!callState.isConnected) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
|
// State for overlay mode: compact or preview
|
||||||
|
// Default to true (preview mode) so user sees video immediately after joining
|
||||||
|
final isExpanded = useState(true);
|
||||||
|
|
||||||
|
// If connected, show active call UI
|
||||||
|
if (callState.isConnected) {
|
||||||
|
return _buildActiveCallOverlay(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
callState,
|
||||||
|
callNotifier,
|
||||||
|
isExpanded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not connected but there is an ongoing call, show join prompt
|
||||||
|
if (ongoingCall.value != null) {
|
||||||
|
return _buildJoinPrompt(context, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildJoinPrompt(BuildContext context, WidgetRef ref) {
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.videocam,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Call in progress').bold(),
|
||||||
|
Text('Tap to join', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isLoading.value)
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
).padding(right: 8)
|
||||||
|
else
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// Just join the room, don't navigate
|
||||||
|
await ref.read(callNotifierProvider.notifier).joinRoom(room);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.call, size: 18),
|
||||||
|
label: const Text('Join'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getChatRoomName(SnChatRoom? room, SnAccount currentUser) {
|
||||||
|
if (room == null) return 'unnamed'.tr();
|
||||||
|
return room.name ??
|
||||||
|
(room.members ?? [])
|
||||||
|
.where((element) => element.id != currentUser.id)
|
||||||
|
.map((element) => element.account.nick)
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActiveCallOverlay(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
CallState callState,
|
||||||
|
CallNotifier callNotifier,
|
||||||
|
ValueNotifier<bool> isExpanded,
|
||||||
|
) {
|
||||||
final lastSpeaker =
|
final lastSpeaker =
|
||||||
callNotifier.participants
|
callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? callNotifier.participants.first
|
? callNotifier.participants.firstOrNull
|
||||||
: callNotifier.participants
|
: callNotifier.participants
|
||||||
.where(
|
.where(
|
||||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
)
|
)
|
||||||
.fold(
|
.fold(
|
||||||
callNotifier.participants.first,
|
callNotifier.participants.firstOrNull,
|
||||||
(value, element) =>
|
(value, element) =>
|
||||||
element.remoteParticipant.lastSpokeAt != null &&
|
element.remoteParticipant.lastSpokeAt != null &&
|
||||||
(value.remoteParticipant.lastSpokeAt == null ||
|
(value?.remoteParticipant.lastSpokeAt == null ||
|
||||||
element.remoteParticipant.lastSpokeAt!
|
element.remoteParticipant.lastSpokeAt!
|
||||||
.compareTo(
|
.compareTo(
|
||||||
value
|
value!
|
||||||
.remoteParticipant
|
.remoteParticipant
|
||||||
.lastSpokeAt!,
|
.lastSpokeAt!,
|
||||||
) >
|
) >
|
||||||
@@ -315,11 +441,70 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
: value,
|
: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
final actionButtonStyle = ButtonStyle(
|
if (lastSpeaker == null) return const SizedBox.shrink();
|
||||||
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
final userInfo = ref.watch(userInfoProvider).value!;
|
||||||
|
|
||||||
|
// Preview Mode (Expanded)
|
||||||
|
if (isExpanded.value) {
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Gap(4),
|
||||||
|
Text(_getChatRoomName(callNotifier.chatRoom, userInfo)),
|
||||||
|
const Gap(4),
|
||||||
|
Text(formatDuration(callState.duration)).bold(),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.fullscreen),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CallScreen(room: room),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: 'Full Screen',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.expand_less),
|
||||||
|
onPressed: () => isExpanded.value = false,
|
||||||
|
tooltip: 'Collapse',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, vertical: 8),
|
||||||
|
// Video Preview
|
||||||
|
Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: const CallContent(),
|
||||||
|
),
|
||||||
|
const CallControlsBar(
|
||||||
|
isCompact: true,
|
||||||
|
).padding(vertical: 8, horizontal: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact Mode
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
onTap: () => isExpanded.value = true,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -328,12 +513,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Builder(
|
SizedBox(
|
||||||
builder: (context) {
|
|
||||||
if (callNotifier.localParticipant == null) {
|
|
||||||
return CircularProgressIndicator().center();
|
|
||||||
}
|
|
||||||
return SizedBox(
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child:
|
child:
|
||||||
@@ -341,14 +521,19 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
live: lastSpeaker,
|
live: lastSpeaker,
|
||||||
size: 36,
|
size: 36,
|
||||||
).center(),
|
).center(),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('@${lastSpeaker.participant.identity}').bold(),
|
Text('@${lastSpeaker.participant.identity}').bold(),
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_getChatRoomName(callNotifier.chatRoom, userInfo),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
formatDuration(callState.duration),
|
formatDuration(callState.duration),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
@@ -357,45 +542,26 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.toggleMicrophone();
|
callNotifier.toggleMicrophone();
|
||||||
},
|
},
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more),
|
||||||
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
onPressed: () => isExpanded.value = true,
|
||||||
),
|
tooltip: 'Expand',
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleCamera();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isScreenSharing
|
|
||||||
? Icons.stop_screen_share
|
|
||||||
: Icons.screen_share,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleScreenShare(context);
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(all: 16),
|
).padding(all: 12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
context.pushNamed(
|
|
||||||
'chatCall',
|
|
||||||
pathParameters: {'id': callNotifier.roomId!},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ PODS:
|
|||||||
- OrderedSet (~> 6.0.3)
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_local_notifications (0.0.1):
|
- flutter_local_notifications (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_platform_alert (0.0.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- flutter_secure_storage_macos (6.1.3):
|
- flutter_secure_storage_macos (6.1.3):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_timezone (0.1.0):
|
- flutter_timezone (0.1.0):
|
||||||
@@ -269,7 +267,6 @@ DEPENDENCIES:
|
|||||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
|
|
||||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
||||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||||
@@ -349,8 +346,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
flutter_platform_alert:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
|
|
||||||
flutter_secure_storage_macos:
|
flutter_secure_storage_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
@@ -432,7 +427,6 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||||
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
||||||
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
|
|
||||||
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||||
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
||||||
flutter_udid: 00c09e022fd527fd39fef97670b220f2ae8190e7
|
flutter_udid: 00c09e022fd527fd39fef97670b220f2ae8190e7
|
||||||
|
|||||||
Reference in New Issue
Block a user