Floating call widgets

This commit is contained in:
2024-04-30 20:31:54 +08:00
parent 5922d325e5
commit 7fb94eeafa
16 changed files with 643 additions and 433 deletions

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:draggable_float_widget/draggable_float_widget.dart';
import 'package:provider/provider.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/router.dart';
class CallOverlay extends StatelessWidget {
const CallOverlay({super.key});
@override
Widget build(BuildContext context) {
const radius = BorderRadius.all(Radius.circular(8));
final chat = context.watch<ChatProvider>();
if (chat.isShown || chat.call == null) {
return Container();
}
return DraggableFloatWidget(
config: const DraggableFloatWidgetBaseConfig(
initPositionYInTop: false,
initPositionYMarginBorder: 50,
borderTopContainTopBar: true,
borderBottom: defaultBorderWidth,
borderLeft: 8,
),
child: Material(
elevation: 6,
color: Colors.transparent,
borderRadius: radius,
child: ClipRRect(
borderRadius: radius,
child: Container(
height: 80,
width: 80,
color: Theme.of(context).colorScheme.secondaryContainer,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.call, size: 18),
const SizedBox(height: 4),
Text(
AppLocalizations.of(context)!.chatCallOngoingShort,
style: const TextStyle(fontSize: 12),
)
],
),
),
),
),
onTap: () {
router.pushNamed(
'chat.channel.call',
extra: chat.call!.info,
pathParameters: {'channel': chat.call!.channel.alias},
);
},
);
}
}

View File

@ -6,6 +6,10 @@ import 'package:flutter_background/flutter_background.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/chat/call/exts.dart';
import 'package:solian/widgets/exts.dart';
class ControlsWidget extends StatefulWidget {
@ -64,11 +68,22 @@ class _ControlsWidgetState extends State<ControlsWidget> {
bool get isMuted => participant.isMuted;
void disconnect() async {
if (await context.showDisconnectDialog() != true) return;
final chat = context.read<ChatProvider>();
if (chat.call != null) {
chat.call!.deactivate();
chat.call!.dispose();
router.pop();
}
}
void disableAudio() async {
await participant.setMicrophoneEnabled(false);
}
Future<void> enableAudio() async {
void enableAudio() async {
await participant.setMicrophoneEnabled(true);
}
@ -207,11 +222,17 @@ class _ControlsWidgetState extends State<ControlsWidget> {
spacing: 5,
runSpacing: 5,
children: [
IconButton(
icon: Transform.flip(flipX: true, child: const Icon(Icons.exit_to_app)),
color: Theme.of(context).colorScheme.onSurface,
onPressed: disconnect,
),
if (participant.isMicrophoneEnabled())
if (lkPlatformIs(PlatformType.android))
IconButton(
onPressed: disableAudio,
icon: const Icon(Icons.mic),
color: Theme.of(context).colorScheme.onSurface,
tooltip: AppLocalizations.of(context)!.chatCallMute,
)
else
@ -247,43 +268,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
IconButton(
onPressed: enableAudio,
icon: const Icon(Icons.mic_off),
color: Theme.of(context).colorScheme.onSurface,
tooltip: AppLocalizations.of(context)!.chatCallUnMute,
),
if (!lkPlatformIs(PlatformType.iOS))
PopupMenuButton<MediaDevice>(
icon: const Icon(Icons.volume_up),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem<MediaDevice>(
value: null,
child: ListTile(
leading: Icon(Icons.speaker),
title: Text('Select Audio Output'),
),
),
if (_audioOutputs != null)
..._audioOutputs!.map((device) {
return PopupMenuItem<MediaDevice>(
value: device,
child: ListTile(
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank),
title: Text(device.label),
),
onTap: () => selectAudioOutput(device),
);
})
];
},
),
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
IconButton(
disabledColor: Colors.grey,
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
),
if (participant.isCameraEnabled())
PopupMenuButton<MediaDevice>(
icon: const Icon(Icons.videocam_sharp),
@ -317,22 +304,61 @@ class _ControlsWidgetState extends State<ControlsWidget> {
IconButton(
onPressed: enableVideo,
icon: const Icon(Icons.videocam_off),
color: Theme.of(context).colorScheme.onSurface,
tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
),
IconButton(
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front),
color: Theme.of(context).colorScheme.onSurface,
onPressed: () => toggleCamera(),
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
),
if (!lkPlatformIs(PlatformType.iOS))
PopupMenuButton<MediaDevice>(
icon: const Icon(Icons.volume_up),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem<MediaDevice>(
value: null,
child: ListTile(
leading: Icon(Icons.speaker),
title: Text('Select Audio Output'),
),
),
if (_audioOutputs != null)
..._audioOutputs!.map((device) {
return PopupMenuItem<MediaDevice>(
value: device,
child: ListTile(
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank),
title: Text(device.label),
),
onTap: () => selectAudioOutput(device),
);
})
];
},
),
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
IconButton(
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
color: Theme.of(context).colorScheme.onSurface,
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
),
if (participant.isScreenShareEnabled())
IconButton(
icon: const Icon(Icons.monitor_outlined),
color: Theme.of(context).colorScheme.onSurface,
onPressed: () => disableScreenShare(),
tooltip: AppLocalizations.of(context)!.chatCallScreenOff,
)
else
IconButton(
icon: const Icon(Icons.monitor),
color: Theme.of(context).colorScheme.onSurface,
onPressed: () => enableScreenShare(),
tooltip: AppLocalizations.of(context)!.chatCallScreenOn,
),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension SolianCallExt on BuildContext {
Future<bool?> showPlayAudioManuallyDialog() => showDialog<bool>(
@ -24,16 +25,16 @@ extension SolianCallExt on BuildContext {
Future<bool?> showDisconnectDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Disconnect'),
content: const Text('Are you sure to disconnect?'),
title: Text(AppLocalizations.of(this)!.chatCallDisconnect),
content: Text(AppLocalizations.of(this)!.chatCallDisconnectConfirm),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
child: Text(AppLocalizations.of(this)!.confirmCancel),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Disconnect'),
child: Text(AppLocalizations.of(this)!.confirmOkay),
),
],
),

View File

@ -5,7 +5,7 @@ class LayoutWrapper extends StatelessWidget {
final Widget? child;
final Widget? floatingActionButton;
final List<Widget>? appBarActions;
final bool? noSafeArea;
final bool noSafeArea;
final String title;
const LayoutWrapper({
@ -14,7 +14,7 @@ class LayoutWrapper extends StatelessWidget {
required this.title,
this.floatingActionButton,
this.appBarActions,
this.noSafeArea,
this.noSafeArea = false,
});
@override
@ -25,7 +25,7 @@ class LayoutWrapper extends StatelessWidget {
appBar: AppBar(title: Text(title), actions: appBarActions),
floatingActionButton: floatingActionButton,
drawer: const SolianNavigationDrawer(),
body: (noSafeArea ?? false) ? content : SafeArea(child: content),
body: noSafeArea ? content : SafeArea(child: content),
);
}
}

View File

@ -3,16 +3,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension SolianCommonExtensions on BuildContext {
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
context: this,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(this)!.errorHappened),
content: Text(exception.toString()),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(AppLocalizations.of(this)!.confirmOkay),
)
],
),
);
}
context: this,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(this)!.errorHappened),
content: Text(exception.toString()),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(AppLocalizations.of(this)!.confirmOkay),
)
],
),
);
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/common_wrapper.dart';
import 'package:solian/widgets/navigation_drawer.dart';
class IndentWrapper extends LayoutWrapper {
final bool? hideDrawer;
final bool hideDrawer;
const IndentWrapper({
super.key,
@ -11,8 +12,8 @@ class IndentWrapper extends LayoutWrapper {
required super.title,
super.floatingActionButton,
super.appBarActions,
this.hideDrawer,
super.noSafeArea,
this.hideDrawer = false,
super.noSafeArea = false,
}) : super();
@override
@ -20,10 +21,17 @@ class IndentWrapper extends LayoutWrapper {
final content = child ?? Container();
return Scaffold(
appBar: AppBar(title: Text(title), actions: appBarActions),
appBar: AppBar(
leading: hideDrawer ? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => router.pop(),
) : null,
title: Text(title),
actions: appBarActions,
),
floatingActionButton: floatingActionButton,
drawer: (hideDrawer ?? false) ? null : const SolianNavigationDrawer(),
body: (noSafeArea ?? false) ? content : SafeArea(child: content),
drawer: const SolianNavigationDrawer(),
body: noSafeArea ? content : SafeArea(child: content),
);
}
}

View File

@ -62,7 +62,9 @@ class _AttachmentItemState extends State<AttachmentItem> {
: Positioned(
right: 12,
bottom: 8,
child: Chip(label: Text(widget.badge!)),
child: Material(
child: Chip(label: Text(widget.badge!)),
),
)
],
),