🐛 Bug fixes
This commit is contained in:
		| @@ -5,6 +5,8 @@ PODS: | ||||
|   - device_info_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_local_notifications (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_secure_storage (6.0.0): | ||||
|     - Flutter | ||||
|   - flutter_webrtc (0.9.36): | ||||
| @@ -44,6 +46,7 @@ DEPENDENCIES: | ||||
|   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) | ||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) | ||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||
| @@ -71,6 +74,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/device_info_plus/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_local_notifications: | ||||
|     :path: ".symlinks/plugins/flutter_local_notifications/ios" | ||||
|   flutter_secure_storage: | ||||
|     :path: ".symlinks/plugins/flutter_secure_storage/ios" | ||||
|   flutter_webrtc: | ||||
| @@ -106,6 +111,7 @@ SPEC CHECKSUMS: | ||||
|   connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db | ||||
|   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 | ||||
|   flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be | ||||
|   flutter_webrtc: 9bc044b0b5bcaabd0fb7d52c90421fb540f8c35e | ||||
|   image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "solian": "Solian", | ||||
|   "appName": "Solar Network", | ||||
|   "explore": "Explore", | ||||
|   "chat": "Chat", | ||||
|   "account": "Account", | ||||
| @@ -28,6 +28,7 @@ | ||||
|   "report": "Report", | ||||
|   "reply": "Reply", | ||||
|   "settings": "Settings", | ||||
|   "errorHappened": "An Error Occurred", | ||||
|   "notification": "Notification", | ||||
|   "notifyDone": "You're done!", | ||||
|   "notifyDoneCaption": "There are no notifications unread for you.", | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "solian": "索链", | ||||
|   "appName": "Solar", | ||||
|   "explore": "探索", | ||||
|   "chat": "聊天", | ||||
|   "account": "账号", | ||||
| @@ -28,6 +28,7 @@ | ||||
|   "report": "举报", | ||||
|   "reply": "回复", | ||||
|   "settings": "设置", | ||||
|   "errorHappened": "发生了错误", | ||||
|   "notification": "通知", | ||||
|   "notifyDone": "所有通知已读!", | ||||
|   "notifyDoneCaption": "这里没有什么东西可以给你看的了~", | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| @@ -24,17 +25,25 @@ class NotifyProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   void initNotify() { | ||||
|     const androidSettings = AndroidInitializationSettings('app_icon'); | ||||
|     const darwinSettings = DarwinInitializationSettings( | ||||
|       notificationCategories: [ | ||||
|         DarwinNotificationCategory("general"), | ||||
|       ], | ||||
|     ); | ||||
|     const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|     const InitializationSettings initializationSettings = InitializationSettings( | ||||
|       android: AndroidInitializationSettings('app_icon'), | ||||
|       iOS: DarwinInitializationSettings(), | ||||
|       macOS: DarwinInitializationSettings(), | ||||
|       linux: LinuxInitializationSettings(defaultActionName: 'Open notification'), | ||||
|       android: androidSettings, | ||||
|       iOS: darwinSettings, | ||||
|       macOS: darwinSettings, | ||||
|       linux: linuxSettings, | ||||
|     ); | ||||
|  | ||||
|     localNotify.initialize(initializationSettings); | ||||
|   } | ||||
|  | ||||
|   Future<void> requestPermissions() async { | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; | ||||
|     await Permission.notification.request(); | ||||
|   } | ||||
|  | ||||
| @@ -78,20 +87,30 @@ class NotifyProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   void notifyMessage(String title, String body) { | ||||
|     const androidSettings = AndroidNotificationDetails( | ||||
|       'general', | ||||
|       'General', | ||||
|       importance: Importance.high, | ||||
|       priority: Priority.high, | ||||
|       silent: true, | ||||
|     ); | ||||
|     const darwinSettings = DarwinNotificationDetails( | ||||
|       presentAlert: true, | ||||
|       presentBanner: true, | ||||
|       presentBadge: true, | ||||
|       presentSound: false, | ||||
|     ); | ||||
|     const linuxSettings = LinuxNotificationDetails(); | ||||
|  | ||||
|     localNotify.show( | ||||
|       math.max(1, math.Random().nextInt(100000000)), | ||||
|       title, | ||||
|       body, | ||||
|       const NotificationDetails( | ||||
|         android: AndroidNotificationDetails( | ||||
|           'general', | ||||
|           'General', | ||||
|           importance: Importance.high, | ||||
|           priority: Priority.high, | ||||
|         ), | ||||
|         iOS: DarwinNotificationDetails(), | ||||
|         macOS: DarwinNotificationDetails(), | ||||
|         linux: LinuxNotificationDetails(), | ||||
|         android: androidSettings, | ||||
|         iOS: darwinSettings, | ||||
|         macOS: darwinSettings, | ||||
|         linux: linuxSettings, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:solian/models/friendship.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/account/avatar.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| @@ -40,9 +41,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       }); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -65,9 +64,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       await fetchFriendships(); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|  | ||||
|     setState(() => _isSubmitting = false); | ||||
| @@ -97,9 +94,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       await fetchFriendships(); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|  | ||||
|     setState(() => _isSubmitting = false); | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:solian/widgets/chat/call/controls.dart'; | ||||
| import 'package:solian/widgets/chat/call/exts.dart'; | ||||
| import 'package:solian/widgets/chat/call/participant.dart'; | ||||
| import 'package:solian/widgets/chat/call/participant_menu.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| @@ -81,9 +82,7 @@ class _ChatCallState extends State<ChatCall> { | ||||
|       return (_token!, _endpoint!); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } | ||||
|   } | ||||
| @@ -138,10 +137,7 @@ class _ChatCallState extends State<ChatCall> { | ||||
|  | ||||
|       setupRoom(); | ||||
|     } catch (e) { | ||||
|       final message = e.toString(); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(e); | ||||
|     } finally { | ||||
|       notify.close(); | ||||
|     } | ||||
| @@ -182,9 +178,6 @@ class _ChatCallState extends State<ChatCall> { | ||||
|         if (router.canPop()) router.pop(); | ||||
|       }) | ||||
|       ..on<ParticipantEvent>((event) => sortParticipants()) | ||||
|       ..on<RoomRecordingStatusChanged>((event) { | ||||
|         context.showRecordingStatusChangedDialog(event.activeRecording); | ||||
|       }) | ||||
|       ..on<LocalTrackPublishedEvent>((_) => sortParticipants()) | ||||
|       ..on<LocalTrackUnpublishedEvent>((_) => sortParticipants()) | ||||
|       ..on<TrackSubscribedEvent>((_) => sortParticipants()) | ||||
| @@ -203,30 +196,22 @@ class _ChatCallState extends State<ChatCall> { | ||||
|   } | ||||
|  | ||||
|   void sortParticipants() { | ||||
|     List<ParticipantTrack> screenTracks = []; | ||||
|     Map<String, ParticipantTrack> userMediaTracks = {}; | ||||
|     Map<String, ParticipantTrack> mediaTracks = {}; | ||||
|     for (var participant in _callRoom.remoteParticipants.values) { | ||||
|       userMediaTracks[participant.sid] = ParticipantTrack( | ||||
|       mediaTracks[participant.sid] = ParticipantTrack( | ||||
|         participant: participant, | ||||
|         videoTrack: null, | ||||
|         isScreenShare: false, | ||||
|       ); | ||||
|  | ||||
|       for (var t in participant.videoTrackPublications) { | ||||
|         if (t.isScreenShare) { | ||||
|           screenTracks.add(ParticipantTrack( | ||||
|             participant: participant, | ||||
|             videoTrack: t.track as VideoTrack, | ||||
|             isScreenShare: true, | ||||
|           )); | ||||
|         } else { | ||||
|           userMediaTracks[participant.sid]?.videoTrack = t.track; | ||||
|         } | ||||
|         mediaTracks[participant.sid]?.videoTrack = t.track; | ||||
|         mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     final userMediaTrackList = userMediaTracks.values.toList(); | ||||
|     userMediaTrackList.sort((a, b) { | ||||
|     final mediaTrackList = mediaTracks.values.toList(); | ||||
|     mediaTrackList.sort((a, b) { | ||||
|       // Loudest people first | ||||
|       if (a.participant.isSpeaking && b.participant.isSpeaking) { | ||||
|         if (a.participant.audioLevel > b.participant.audioLevel) { | ||||
| @@ -262,22 +247,20 @@ class _ChatCallState extends State<ChatCall> { | ||||
|       final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications; | ||||
|       if (localParticipantTracks != null) { | ||||
|         for (var t in localParticipantTracks) { | ||||
|           if (t.isScreenShare) { | ||||
|             screenTracks.add(ParticipantTrack( | ||||
|               participant: _callRoom.localParticipant!, | ||||
|               videoTrack: t.track as VideoTrack, | ||||
|               isScreenShare: true, | ||||
|             )); | ||||
|           } else { | ||||
|             localTrack.videoTrack = t.track; | ||||
|           } | ||||
|           localTrack.videoTrack = t.track; | ||||
|           localTrack.isScreenShare = t.isScreenShare; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     setState(() { | ||||
|       _participantTracks = [...screenTracks, localTrack, ...userMediaTrackList]; | ||||
|       _focusParticipant ??= _participantTracks.first; | ||||
|       _participantTracks = [localTrack, ...mediaTrackList]; | ||||
|       if (_focusParticipant == null) { | ||||
|         _focusParticipant = _participantTracks.first; | ||||
|       } else { | ||||
|         final idx = _participantTracks.indexWhere((x) => _focusParticipant!.participant.sid == x.participant.sid); | ||||
|         _focusParticipant = _participantTracks[idx]; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -466,6 +449,7 @@ class InteractiveParticipantWidget extends StatelessWidget { | ||||
|   final double? width; | ||||
|   final double? height; | ||||
|   final Color? color; | ||||
|   final bool? isFixed; | ||||
|   final ParticipantTrack participant; | ||||
|   final Function() onTap; | ||||
|  | ||||
| @@ -474,6 +458,7 @@ class InteractiveParticipantWidget extends StatelessWidget { | ||||
|     this.width, | ||||
|     this.height, | ||||
|     this.color, | ||||
|     this.isFixed = false, | ||||
|     required this.participant, | ||||
|     required this.onTap, | ||||
|   }); | ||||
| @@ -485,7 +470,7 @@ class InteractiveParticipantWidget extends StatelessWidget { | ||||
|         width: width, | ||||
|         height: height, | ||||
|         color: color, | ||||
|         child: ParticipantWidget.widgetFor(participant), | ||||
|         child: ParticipantWidget.widgetFor(participant, isFixed: true), | ||||
|       ), | ||||
|       onTap: () => onTap(), | ||||
|       onLongPress: () { | ||||
|   | ||||
| @@ -8,6 +8,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'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
| @@ -52,9 +53,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|     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 { | ||||
|       if (router.canPop()) { | ||||
|         router.pop(true); | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/account/avatar.dart'; | ||||
| import 'package:solian/widgets/account/friend_picker.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| @@ -45,9 +46,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|       }); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -75,9 +74,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|       await fetchMemberships(); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|  | ||||
|     setState(() => _isSubmitting = false); | ||||
| @@ -107,9 +104,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|       await fetchMemberships(); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|  | ||||
|     setState(() => _isSubmitting = false); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import 'package:solian/widgets/chat/maintainer.dart'; | ||||
| import 'package:solian/widgets/chat/message.dart'; | ||||
| import 'package:solian/widgets/chat/message_action.dart'; | ||||
| import 'package:solian/widgets/chat/message_editor.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
| @@ -46,9 +47,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       return _channelMeta!; | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } | ||||
|   } | ||||
| @@ -62,9 +61,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       return _ongoingCall; | ||||
|     } else if (res.statusCode != 404) { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } else { | ||||
|       return null; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/chat/chat_new.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/widgets/notification_notifier.dart'; | ||||
| @@ -36,9 +37,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> { | ||||
|       }); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $message")), | ||||
|       ); | ||||
|       context.showErrorDialog(message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,10 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     final nty = context.watch<NotifyProvider>(); | ||||
|  | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       nty.allRead(); | ||||
|     }); | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       noSafeArea: true, | ||||
|       hideDrawer: true, | ||||
| @@ -48,9 +52,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                       return NotificationItem( | ||||
|                         index: index, | ||||
|                         item: element, | ||||
|                         onDismiss: () => setState(() { | ||||
|                           nty.clearAt(index); | ||||
|                         }), | ||||
|                         onDismiss: () => nty.clearAt(index), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
| @@ -69,14 +71,6 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     final nty = context.read<NotifyProvider>(); | ||||
|     nty.allRead(); | ||||
|     nty.clearRealtime(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class NotificationItem extends StatelessWidget { | ||||
| @@ -141,7 +135,7 @@ class NotificationItem extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Dismissible( | ||||
|       key: Key('n$index'), | ||||
|       key: Key((DateTime.now().millisecondsSinceEpoch << 10).toString()), | ||||
|       onDismissed: (direction) { | ||||
|         markAsRead(item, context).then((value) { | ||||
|           ScaffoldMessenger.of(context).showSnackBar( | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:solian/router.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/widgets/account/avatar.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:solian/widgets/posts/attachment_editor.dart'; | ||||
|  | ||||
| @@ -75,9 +76,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | ||||
|     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 { | ||||
|       if (router.canPop()) { | ||||
|         router.pop(true); | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:solian/router.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/widgets/account/avatar.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:solian/widgets/posts/attachment_editor.dart'; | ||||
|  | ||||
| @@ -65,9 +66,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | ||||
|     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 { | ||||
|       if (router.canPop()) { | ||||
|         router.pop(true); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; | ||||
| import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:solian/widgets/posts/comment_list.dart'; | ||||
| import 'package:solian/widgets/posts/item.dart'; | ||||
| @@ -30,9 +31,7 @@ class _PostScreenState extends State<PostScreen> { | ||||
|     final res = await _client.get(uri); | ||||
|     if (res.statusCode != 200) { | ||||
|       final err = utf8.decode(res.bodyBytes); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $err")), | ||||
|       ); | ||||
|       context.showErrorDialog(err); | ||||
|       return null; | ||||
|     } else { | ||||
|       return Post.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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'), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
| } | ||||
| @@ -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, | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										18
									
								
								lib/widgets/exts.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								lib/widgets/exts.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| 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), | ||||
|         ) | ||||
|       ], | ||||
|     ), | ||||
|   ); | ||||
| } | ||||
| @@ -72,7 +72,7 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> { | ||||
|               Image.asset("assets/logo.png", width: 26, height: 26), | ||||
|               const SizedBox(width: 10), | ||||
|               Text( | ||||
|                 AppLocalizations.of(context)!.solian, | ||||
|                 AppLocalizations.of(context)!.appName, | ||||
|                 style: const TextStyle(fontWeight: FontWeight.w900), | ||||
|               ), | ||||
|             ], | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
|  | ||||
| class AttachmentEditor extends StatefulWidget { | ||||
|   final String provider; | ||||
| @@ -67,9 +68,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|     try { | ||||
|       await uploadAttachment(file, hashcode); | ||||
|     } catch (err) { | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $err")), | ||||
|       ); | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isSubmitting = false); | ||||
|     } | ||||
| @@ -94,9 +93,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|     try { | ||||
|       await uploadAttachment(file, hashcode); | ||||
|     } catch (err) { | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $err")), | ||||
|       ); | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isSubmitting = false); | ||||
|     } | ||||
| @@ -133,9 +130,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|       widget.onUpdate(_attachments); | ||||
|     } else { | ||||
|       final err = utf8.decode(await res.stream.toBytes()); | ||||
|       ScaffoldMessenger.of(context).showSnackBar( | ||||
|         SnackBar(content: Text("Something went wrong... $err")), | ||||
|       ); | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|     setState(() => _isSubmitting = false); | ||||
|   } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:solian/models/post.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 ItemDeletionDialog extends StatefulWidget { | ||||
|   final Post item; | ||||
| @@ -35,9 +36,7 @@ class _ItemDeletionDialogState extends State<ItemDeletionDialog> { | ||||
|     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); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:solian/models/reaction.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
|  | ||||
| Future<void> doReact( | ||||
|   String dataset, | ||||
| @@ -51,9 +52,7 @@ Future<void> doReact( | ||||
|     ); | ||||
|   } else { | ||||
|     final message = utf8.decode(res.bodyBytes); | ||||
|     ScaffoldMessenger.of(context).showSnackBar( | ||||
|       SnackBar(content: Text("Something went wrong... $message")), | ||||
|     ); | ||||
|     context.showErrorDialog(message); | ||||
|   } | ||||
|  | ||||
|   if (Navigator.canPop(context)) { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | ||||
| 	<true/> | ||||
| 	<false/> | ||||
| 	<key>CFBundleDevelopmentRegion</key> | ||||
| 	<string>$(DEVELOPMENT_LANGUAGE)</string> | ||||
| 	<key>CFBundleExecutable</key> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user