✨ Call in same screen on large screen
This commit is contained in:
parent
c82c48dfec
commit
253cd1ecbd
@ -17,6 +17,10 @@ class ChatCallProvider extends GetxController {
|
||||
RxBool isReady = false.obs;
|
||||
RxBool isMounted = false.obs;
|
||||
RxBool isInitialized = false.obs;
|
||||
RxBool isBusy = false.obs;
|
||||
|
||||
RxString lastDuration = '00:00:00'.obs;
|
||||
Timer? lastDurationUpdateTimer;
|
||||
|
||||
String? token;
|
||||
String? endpoint;
|
||||
@ -38,6 +42,34 @@ class ChatCallProvider extends GetxController {
|
||||
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
|
||||
Rx<ParticipantTrack?> focusTrack = Rx(null);
|
||||
|
||||
void _updateDuration() {
|
||||
if (current.value == null) {
|
||||
lastDuration.value = '00:00:00';
|
||||
return;
|
||||
}
|
||||
|
||||
Duration duration = DateTime.now().difference(current.value!.createdAt);
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
String formattedTime = '${twoDigits(duration.inHours)}:'
|
||||
'${twoDigits(duration.inMinutes.remainder(60))}:'
|
||||
'${twoDigits(duration.inSeconds.remainder(60))}';
|
||||
lastDuration.value = formattedTime;
|
||||
}
|
||||
|
||||
void enableDurationUpdater() {
|
||||
_updateDuration();
|
||||
lastDurationUpdateTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => _updateDuration(),
|
||||
);
|
||||
}
|
||||
|
||||
void disableDurationUpdater() {
|
||||
lastDurationUpdateTimer?.cancel();
|
||||
lastDurationUpdateTimer = null;
|
||||
}
|
||||
|
||||
Future<void> checkPermissions() async {
|
||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
|
||||
return;
|
||||
@ -119,8 +151,6 @@ class ChatCallProvider extends GetxController {
|
||||
void joinRoom(String url, String token) async {
|
||||
if (isMounted.value) {
|
||||
return;
|
||||
} else {
|
||||
isMounted.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -134,6 +164,8 @@ class ChatCallProvider extends GetxController {
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
isMounted.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +197,7 @@ class ChatCallProvider extends GetxController {
|
||||
Hardware.instance.setSpeakerphoneOn(true);
|
||||
}
|
||||
|
||||
isBusy.value = false;
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
||||
|
@ -12,16 +12,15 @@ import 'package:solian/widgets/chat/call/call_participant.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||
|
||||
class CallScreen extends StatefulWidget {
|
||||
const CallScreen({super.key});
|
||||
final bool hideAppBar;
|
||||
|
||||
const CallScreen({super.key, this.hideAppBar = false});
|
||||
|
||||
@override
|
||||
State<CallScreen> createState() => _CallScreenState();
|
||||
}
|
||||
|
||||
class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
String _currentDuration = '00:00:00';
|
||||
|
||||
int _layoutMode = 0;
|
||||
|
||||
bool _showControls = true;
|
||||
@ -37,26 +36,6 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
|
||||
String _parseDuration() {
|
||||
final ChatCallProvider provider = Get.find();
|
||||
if (provider.current.value == null) return '00:00:00';
|
||||
Duration duration =
|
||||
DateTime.now().difference(provider.current.value!.createdAt);
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
String formattedTime = '${twoDigits(duration.inHours)}:'
|
||||
'${twoDigits(duration.inMinutes.remainder(60))}:'
|
||||
'${twoDigits(duration.inSeconds.remainder(60))}';
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
void _updateDuration() {
|
||||
setState(() {
|
||||
_currentDuration = _parseDuration();
|
||||
});
|
||||
}
|
||||
|
||||
void _switchLayout() {
|
||||
if (_layoutMode < 1) {
|
||||
setState(() => _layoutMode++);
|
||||
@ -191,15 +170,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Get.find<ChatCallProvider>().setupRoom();
|
||||
super.initState();
|
||||
|
||||
_updateDuration();
|
||||
Future.delayed(Duration.zero, () {
|
||||
Get.find<ChatCallProvider>()
|
||||
..setupRoom()
|
||||
..enableDurationUpdater();
|
||||
|
||||
_planAutoHideControls();
|
||||
_timer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => _updateDuration(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -210,16 +189,19 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ChatCallProvider provider = Get.find();
|
||||
final ChatCallProvider ctrl = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: widget.hideAppBar
|
||||
? null
|
||||
: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
centerTitle: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
title: RichText(
|
||||
title: Obx(
|
||||
() => RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
@ -228,12 +210,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: _currentDuration,
|
||||
text: ctrl.lastDuration.value,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
@ -259,13 +242,20 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
Obx(() {
|
||||
return Row(
|
||||
children: [
|
||||
Text(call.room.serverRegion ?? 'unknown'),
|
||||
const SizedBox(width: 6),
|
||||
Text(call.room.serverVersion ?? 'unknown')
|
||||
],
|
||||
Text(
|
||||
call.channel.value!.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(call.lastDuration.value)
|
||||
],
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
@ -332,7 +322,6 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
elevation: 2,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
@ -345,15 +334,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (provider.room.localParticipant != null)
|
||||
if (ctrl.room.localParticipant != null)
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
provider.room,
|
||||
provider.room.localParticipant!,
|
||||
ctrl.room,
|
||||
ctrl.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -370,17 +359,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
Get.find<ChatCallProvider>().disableDurationUpdater();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void activate() {
|
||||
_timer ??= Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => _updateDuration(),
|
||||
);
|
||||
Get.find<ChatCallProvider>().enableDurationUpdater();
|
||||
super.activate();
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,11 @@ import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/call.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/channel/call/call.dart';
|
||||
import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
@ -39,7 +41,7 @@ class ChannelChatScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
with WidgetsBindingObserver {
|
||||
with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
DateTime? _isOutOfSyncSince;
|
||||
|
||||
bool _isBusy = false;
|
||||
@ -238,12 +240,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_ongoingCall != null)
|
||||
ChannelCallIndicator(
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall!,
|
||||
onJoin: () {
|
||||
if (!SolianTheme.isLargeScreen(context)) {
|
||||
final ChatCallProvider call = Get.find();
|
||||
call.gotoScreen(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ChatEventList(
|
||||
@ -261,7 +272,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
if (_isOutOfSyncSince != null)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
tileColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
leading: const Icon(Icons.history_toggle_off),
|
||||
title: Text('messageOutOfSync'.tr),
|
||||
subtitle: Text('messageOutOfSyncCaption'.tr),
|
||||
@ -310,6 +322,23 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (call.isMounted.value && SolianTheme.isLargeScreen(context)) {
|
||||
return const Expanded(
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
Expanded(
|
||||
child: CallScreen(hideAppBar: true),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
@ -269,6 +269,7 @@ const i18nEnglish = {
|
||||
'callOngoing': 'A call is ongoing...',
|
||||
'callOngoingEmpty': 'A call is on hold...',
|
||||
'callOngoingParticipants': '@count people are calling...',
|
||||
'callOngoingJoined': 'Call last @duration',
|
||||
'callJoin': 'Join',
|
||||
'callResume': 'Resume',
|
||||
'callMicrophone': 'Microphone',
|
||||
|
@ -247,6 +247,7 @@ const i18nSimplifiedChinese = {
|
||||
'callOngoing': '一则通话正在进行中…',
|
||||
'callOngoingEmpty': '一则通话待机中…',
|
||||
'callOngoingParticipants': '@count 人正在进行通话…',
|
||||
'callOngoingJoined': '通话进行 @duration',
|
||||
'callJoin': '加入',
|
||||
'callResume': '恢复',
|
||||
'callMicrophone': '麦克风',
|
||||
|
@ -9,14 +9,20 @@ import 'package:solian/models/call.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/call.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/chat/call/call_prejoin.dart';
|
||||
|
||||
class ChannelCallIndicator extends StatelessWidget {
|
||||
final Channel channel;
|
||||
final Call ongoingCall;
|
||||
final Function onJoin;
|
||||
|
||||
const ChannelCallIndicator(
|
||||
{super.key, required this.channel, required this.ongoingCall});
|
||||
const ChannelCallIndicator({
|
||||
super.key,
|
||||
required this.channel,
|
||||
required this.ongoingCall,
|
||||
required this.onJoin,
|
||||
});
|
||||
|
||||
void _showCallPrejoin(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
@ -40,48 +46,72 @@ class ChannelCallIndicator extends StatelessWidget {
|
||||
dividerColor: Colors.transparent,
|
||||
content: Row(
|
||||
children: [
|
||||
if (ongoingCall.participants.isEmpty) Text('callOngoingEmpty'.tr),
|
||||
if (ongoingCall.participants.isNotEmpty)
|
||||
Text('callOngoingParticipants'.trParams({
|
||||
Obx(() {
|
||||
if (call.isInitialized.value) {
|
||||
return Text('callOngoingJoined'.trParams({
|
||||
'duration': call.lastDuration.value,
|
||||
}));
|
||||
} else if (ongoingCall.participants.isEmpty) {
|
||||
return Text('callOngoingEmpty'.tr);
|
||||
} else {
|
||||
return Text('callOngoingParticipants'.trParams({
|
||||
'count': ongoingCall.participants.length.toString(),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 6),
|
||||
if (ongoingCall.participants.isNotEmpty)
|
||||
Container(
|
||||
Obx(() {
|
||||
if (call.isInitialized.value) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (ongoingCall.participants.isNotEmpty) {
|
||||
return Container(
|
||||
height: 28,
|
||||
constraints: const BoxConstraints(maxWidth: 120),
|
||||
child: AvatarStack(
|
||||
height: 28,
|
||||
borderWidth: 0,
|
||||
avatars: ongoingCall.participants.map((x) {
|
||||
final userinfo = Account.fromJson(jsonDecode(x['metadata']));
|
||||
final userinfo =
|
||||
Account.fromJson(jsonDecode(x['metadata']));
|
||||
return PlatformInfo.canCacheImage
|
||||
? CachedNetworkImageProvider(userinfo.avatar)
|
||||
as ImageProvider
|
||||
: NetworkImage(userinfo.avatar) as ImageProvider;
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
})
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Obx(() {
|
||||
if (call.current.value == null) {
|
||||
if (call.isBusy.value) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
).paddingAll(16);
|
||||
} else if (call.current.value == null) {
|
||||
return TextButton(
|
||||
onPressed: () => _showCallPrejoin(context),
|
||||
child: Text('callJoin'.tr),
|
||||
);
|
||||
} else if (call.channel.value?.id == channel.id) {
|
||||
} else if (call.channel.value?.id == channel.id &&
|
||||
!SolianTheme.isLargeScreen(context)) {
|
||||
return TextButton(
|
||||
onPressed: () => call.gotoScreen(context),
|
||||
onPressed: () => onJoin(),
|
||||
child: Text('callResume'.tr),
|
||||
);
|
||||
} else {
|
||||
} else if (!SolianTheme.isLargeScreen(context)) {
|
||||
return TextButton(
|
||||
onPressed: null,
|
||||
child: Text('callJoin'.tr),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
})
|
||||
],
|
||||
);
|
||||
|
@ -88,12 +88,14 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
void _disconnect() async {
|
||||
if (await showDisconnectDialog() != true) return;
|
||||
|
||||
final ChatCallProvider provider = Get.find();
|
||||
if (provider.current.value != null) {
|
||||
provider.disposeRoom();
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (call.current.value != null) {
|
||||
call.disposeRoom();
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _disableAudio() async {
|
||||
await _participant.setMicrophoneEnabled(false);
|
||||
@ -209,8 +211,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
runSpacing: 5,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Transform.flip(
|
||||
flipX: true, child: const Icon(Icons.exit_to_app)),
|
||||
icon: const Icon(Icons.exit_to_app),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: _disconnect,
|
||||
),
|
||||
|
@ -25,22 +25,23 @@ class ChatCallPrejoinPopup extends StatefulWidget {
|
||||
class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
||||
bool _isBusy = false;
|
||||
|
||||
void performJoin() async {
|
||||
void _performJoin() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final ChatCallProvider provider = Get.find();
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
provider.setCall(widget.ongoingCall, widget.channel);
|
||||
call.setCall(widget.ongoingCall, widget.channel);
|
||||
call.isBusy.value = true;
|
||||
|
||||
try {
|
||||
final resp = await provider.getRoomToken();
|
||||
final resp = await call.getRoomToken();
|
||||
final token = resp.$1;
|
||||
final endpoint = resp.$2;
|
||||
|
||||
provider.initRoom();
|
||||
provider.setupRoomListeners(
|
||||
call.initRoom();
|
||||
call.setupRoomListeners(
|
||||
onDisconnected: (reason) {
|
||||
context.showSnackbar(
|
||||
'callDisconnected'.trParams({'reason': reason.toString()}),
|
||||
@ -48,11 +49,9 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
||||
},
|
||||
);
|
||||
|
||||
provider.joinRoom(endpoint, token);
|
||||
call.joinRoom(endpoint, token);
|
||||
|
||||
provider.gotoScreen(context).then((_) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
@ -62,9 +61,9 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final ChatCallProvider provider = Get.find();
|
||||
provider.checkPermissions().then((_) {
|
||||
provider.initHardware();
|
||||
final ChatCallProvider call = Get.find();
|
||||
call.checkPermissions().then((_) {
|
||||
call.initHardware();
|
||||
});
|
||||
|
||||
super.initState();
|
||||
@ -169,7 +168,7 @@ class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
onPressed: _isBusy ? null : performJoin,
|
||||
onPressed: _isBusy ? null : _performJoin,
|
||||
child: Text('callJoin'.tr),
|
||||
),
|
||||
],
|
||||
|
@ -7,10 +7,10 @@ class ChatCallCurrentIndicator extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ChatCallProvider provider = Get.find();
|
||||
final ChatCallProvider call = Get.find();
|
||||
|
||||
return Obx(() {
|
||||
if (provider.current.value == null || provider.channel.value == null) {
|
||||
if (call.current.value == null || call.channel.value == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@ -18,11 +18,8 @@ class ChatCallCurrentIndicator extends StatelessWidget {
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
leading: const Icon(Icons.call),
|
||||
title: Text(provider.channel.value!.name),
|
||||
title: Text(call.channel.value!.name),
|
||||
subtitle: Text('callAlreadyOngoing'.tr),
|
||||
onTap: () {
|
||||
provider.gotoScreen(context);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -296,8 +296,8 @@ class ChatEvent extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
_buildLinkExpansion().paddingOnly(left: 52),
|
||||
_buildAttachment(context).paddingOnly(left: 56),
|
||||
_buildLinkExpansion().paddingOnly(left: 52, right: 8),
|
||||
_buildAttachment(context).paddingOnly(left: 56, right: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user