🐛 Bug fixes

This commit is contained in:
2024-04-29 20:22:06 +08:00
parent e665b44507
commit 5922d325e5
29 changed files with 249 additions and 380 deletions

View File

@ -6,6 +6,7 @@ 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:solian/widgets/exts.dart';
class ControlsWidget extends StatefulWidget {
final Room room;
@ -134,9 +135,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
await participant.publishVideoTrack(track);
} catch (e) {
final message = e.toString();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Something went wrong... $message'),
));
context.showErrorDialog(message);
}
return;
}

View File

@ -1,206 +1,73 @@
import 'package:flutter/material.dart';
extension SolianCallExt on BuildContext {
Future<bool?> showPublishDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Publish'),
content: const Text('Would you like to publish your Camera & Mic ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('NO'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('YES'),
),
],
),
);
Future<bool?> showPlayAudioManuallyDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Play Audio'),
content: const Text(
'You need to manually activate audio PlayBack for iOS Safari !'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Ignore'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Play Audio'),
),
],
),
);
Future<bool?> showUnPublishDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('UnPublish'),
content:
const Text('Would you like to un-publish your Camera & Mic ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('NO'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('YES'),
),
],
),
);
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Error'),
content: Text(exception.toString()),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
)
],
),
);
Future<bool?> showDisconnectDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Disconnect'),
content: const Text('Are you sure to disconnect?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Disconnect'),
),
],
),
);
Future<bool?> showReconnectDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Reconnect'),
content: const Text('This will force a reconnection'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Reconnect'),
),
],
),
);
Future<void> showReconnectSuccessDialog() => showDialog<void>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Reconnect'),
content: const Text('Reconnection was successful.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
Future<bool?> showSendDataDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Send data'),
content: const Text(
'This will send a sample data to all participants in the room'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Send'),
),
],
),
);
Future<bool?> showDataReceivedDialog(String data) => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Received data'),
content: Text(data),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('OK'),
),
],
),
);
Future<bool?> showRecordingStatusChangedDialog(bool isActiveRecording) =>
showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Room recording reminder'),
content: Text(isActiveRecording
? 'Room recording is active.'
: 'Room recording is stoped.'),
title: const Text('Play Audio'),
content: const Text(
'You need to manually activate audio PlayBack for iOS Safari!',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Ignore'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('OK'),
child: const Text('Play Audio'),
),
],
),
);
Future<bool?> showSubscribePermissionDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Allow subscription'),
content: const Text(
'Allow all participants to subscribe tracks published by local participant?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('NO'),
Future<bool?> showDisconnectDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Disconnect'),
content: const Text('Are you sure to disconnect?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Disconnect'),
),
],
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('YES'),
),
],
),
);
}
);
enum SimulateScenarioResult {
signalReconnect,
fullReconnect,
speakerUpdate,
nodeFailure,
migration,
serverLeave,
switchCandidate,
e2eeKeyRatchet,
participantName,
participantMetadata,
clear,
}
Future<bool?> showReconnectDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Reconnect'),
content: const Text('This will force a reconnection'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Reconnect'),
),
],
),
);
Future<void> showReconnectSuccessDialog() => showDialog<void>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('Reconnect'),
content: const Text('Reconnection was successful.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}

View File

@ -7,8 +7,14 @@ import 'dart:math' as math;
class NoContentWidget extends StatefulWidget {
final Account? userinfo;
final bool isSpeaking;
final bool isFixed;
const NoContentWidget({super.key, this.userinfo, required this.isSpeaking});
const NoContentWidget({
super.key,
this.userinfo,
this.isFixed = false,
required this.isSpeaking,
});
@override
State<NoContentWidget> createState() => _NoContentWidgetState();
@ -35,35 +41,41 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv
@override
Widget build(BuildContext context) {
final radius = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) * 0.1;
final double radius = widget.isFixed
? 16
: math.min(
MediaQuery.of(context).size.width * 0.1,
MediaQuery.of(context).size.height * 0.1,
);
return Container(
alignment: Alignment.center,
child: Center(
child: Animate(
autoPlay: false,
controller: _animationController,
effects: [
CustomEffect(
begin: widget.isSpeaking ? 2 : 0,
end: 8,
curve: Curves.easeInOut,
duration: 1250.ms,
builder: (context, value, child) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
border: value > 0 ? Border.all(color: Colors.green, width: value) : null,
),
child: child,
autoPlay: false,
controller: _animationController,
effects: [
CustomEffect(
begin: widget.isSpeaking ? 2 : 0,
end: 8,
curve: Curves.easeInOut,
duration: 1250.ms,
builder: (context, value, child) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
border: value > 0 ? Border.all(color: Colors.green, width: value) : null,
),
)
],
child: AccountAvatar(
source: widget.userinfo!.avatar,
backgroundColor: Colors.transparent,
radius: radius,
direct: true,
)),
child: child,
),
)
],
child: AccountAvatar(
source: widget.userinfo!.avatar,
backgroundColor: Colors.transparent,
radius: radius,
direct: true,
),
),
),
);
}

View File

@ -10,13 +10,24 @@ import 'package:solian/widgets/chat/call/participant_info.dart';
import 'package:solian/widgets/chat/call/participant_stats.dart';
abstract class ParticipantWidget extends StatefulWidget {
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool showStatsLayer = false}) {
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
{bool isFixed = false, bool showStatsLayer = false}) {
if (participantTrack.participant is LocalParticipant) {
return LocalParticipantWidget(participantTrack.participant as LocalParticipant, participantTrack.videoTrack,
participantTrack.isScreenShare, showStatsLayer);
return LocalParticipantWidget(
participantTrack.participant as LocalParticipant,
participantTrack.videoTrack,
isFixed,
participantTrack.isScreenShare,
showStatsLayer,
);
} else if (participantTrack.participant is RemoteParticipant) {
return RemoteParticipantWidget(participantTrack.participant as RemoteParticipant, participantTrack.videoTrack,
participantTrack.isScreenShare, showStatsLayer);
return RemoteParticipantWidget(
participantTrack.participant as RemoteParticipant,
participantTrack.videoTrack,
isFixed,
participantTrack.isScreenShare,
showStatsLayer,
);
}
throw UnimplementedError('Unknown participant type');
}
@ -24,6 +35,7 @@ abstract class ParticipantWidget extends StatefulWidget {
abstract final Participant participant;
abstract final VideoTrack? videoTrack;
abstract final bool isScreenShare;
abstract final bool isFixed;
abstract final bool showStatsLayer;
final VideoQuality quality;
@ -39,6 +51,8 @@ class LocalParticipantWidget extends ParticipantWidget {
@override
final VideoTrack? videoTrack;
@override
final bool isFixed;
@override
final bool isScreenShare;
@override
final bool showStatsLayer;
@ -46,6 +60,7 @@ class LocalParticipantWidget extends ParticipantWidget {
const LocalParticipantWidget(
this.participant,
this.videoTrack,
this.isFixed,
this.isScreenShare,
this.showStatsLayer, {
super.key,
@ -61,6 +76,8 @@ class RemoteParticipantWidget extends ParticipantWidget {
@override
final VideoTrack? videoTrack;
@override
final bool isFixed;
@override
final bool isScreenShare;
@override
final bool showStatsLayer;
@ -68,6 +85,7 @@ class RemoteParticipantWidget extends ParticipantWidget {
const RemoteParticipantWidget(
this.participant,
this.videoTrack,
this.isFixed,
this.isScreenShare,
this.showStatsLayer, {
super.key,
@ -80,8 +98,6 @@ class RemoteParticipantWidget extends ParticipantWidget {
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> {
VideoTrack? get _activeVideoTrack;
TrackPublication? get _videoPublication;
TrackPublication? get _firstAudioPublication;
Account? _userinfoMetadata;
@ -126,15 +142,14 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
)
: NoContentWidget(
userinfo: _userinfoMetadata,
isFixed: widget.isFixed,
isSpeaking: widget.participant.isSpeaking,
),
if (widget.showStatsLayer)
Positioned(
top: 30,
right: 30,
child: ParticipantStatsWidget(
participant: widget.participant,
),
child: ParticipantStatsWidget(participant: widget.participant),
),
Align(
alignment: Alignment.bottomCenter,
@ -143,9 +158,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
mainAxisSize: MainAxisSize.min,
children: [
ParticipantInfoWidget(
title: widget.participant.name.isNotEmpty
? widget.participant.name
: widget.participant.identity,
title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity,
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare,
@ -159,10 +172,6 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
}
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
@override
LocalTrackPublication<LocalVideoTrack>? get _videoPublication =>
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
@override
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull;
@ -172,10 +181,6 @@ class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticip
}
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
@override
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
@override
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull;

View File

@ -21,17 +21,12 @@ class ParticipantMenu extends StatefulWidget {
}
class _ParticipantMenuState extends State<ParticipantMenu> {
@override
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
@override
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
widget.participant.audioTrackPublications.firstOrNull;
@override
VideoTrack? get _activeVideoTrack => widget.videoTrack;
void tookAction() {
if (Navigator.canPop(context)) {
Navigator.pop(context);

View File

@ -7,6 +7,7 @@ import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
class ChannelCallAction extends StatefulWidget {
final Call? call;
@ -36,9 +37,7 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
var res = await auth.client!.post(uri);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
}
setState(() => _isSubmitting = false);
@ -58,9 +57,7 @@ class _ChannelCallActionState extends State<ChannelCallAction> {
var res = await auth.client!.delete(uri);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
}
setState(() => _isSubmitting = false);

View File

@ -6,6 +6,7 @@ import 'package:solian/models/channel.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
class ChannelDeletion extends StatefulWidget {
final Channel channel;
@ -34,9 +35,7 @@ class _ChannelDeletionState extends State<ChannelDeletion> {
);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
} else if (Navigator.canPop(context)) {
Navigator.pop(context, true);
}
@ -58,9 +57,7 @@ class _ChannelDeletionState extends State<ChannelDeletion> {
);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
} else if (Navigator.canPop(context)) {
Navigator.pop(context, true);
}

View File

@ -6,6 +6,7 @@ import 'package:solian/models/message.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
class ChatMessageDeletionDialog extends StatefulWidget {
final String channel;
@ -34,9 +35,7 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> {
final res = await auth.client!.delete(uri);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
setState(() => _isSubmitting = false);
} else {
Navigator.pop(context, true);

View File

@ -8,6 +8,7 @@ import 'package:solian/models/message.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/posts/attachment_editor.dart';
import 'package:badges/badges.dart' as badge;
@ -64,9 +65,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
var res = await Response.fromStream(await auth.client!.send(req));
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
context.showErrorDialog(message);
} else {
reset();
}