🎨 Formatted all the code
This commit is contained in:
		| @@ -21,8 +21,9 @@ linter: | |||||||
|   # `// ignore_for_file: name_of_lint` syntax on the line or in the file |   # `// ignore_for_file: name_of_lint` syntax on the line or in the file | ||||||
|   # producing the lint. |   # producing the lint. | ||||||
|   rules: |   rules: | ||||||
|     # avoid_print: false  # Uncomment to disable the `avoid_print` rule |     lines_longer_than_80_chars: false | ||||||
|     # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule |     avoid_print: false # Uncomment to disable the `avoid_print` rule | ||||||
|  |     prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule | ||||||
|  |  | ||||||
| # Additional information about this file can be found at | # Additional information about this file can be found at | ||||||
| # https://dart.dev/guides/language/analysis-options | # https://dart.dev/guides/language/analysis-options | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ | |||||||
|   "confirmation": "Confirmation", |   "confirmation": "Confirmation", | ||||||
|   "confirmCancel": "Not sure", |   "confirmCancel": "Not sure", | ||||||
|   "confirmOkay": "OK", |   "confirmOkay": "OK", | ||||||
|  |   "email": "Email Address", | ||||||
|  |   "nickname": "Nickname", | ||||||
|   "username": "Username", |   "username": "Username", | ||||||
|   "password": "Password", |   "password": "Password", | ||||||
|   "next": "Next", |   "next": "Next", | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ | |||||||
|   "confirmation": "确认", |   "confirmation": "确认", | ||||||
|   "confirmCancel": "不太确定", |   "confirmCancel": "不太确定", | ||||||
|   "confirmOkay": "确定", |   "confirmOkay": "确定", | ||||||
|  |   "email": "邮箱地址", | ||||||
|  |   "nickname": "显示名", | ||||||
|   "username": "用户名", |   "username": "用户名", | ||||||
|   "password": "密码", |   "password": "密码", | ||||||
|   "next": "下一步", |   "next": "下一步", | ||||||
|   | |||||||
| @@ -29,7 +29,8 @@ class Call { | |||||||
|         createdAt: DateTime.parse(json["created_at"]), |         createdAt: DateTime.parse(json["created_at"]), | ||||||
|         updatedAt: DateTime.parse(json["updated_at"]), |         updatedAt: DateTime.parse(json["updated_at"]), | ||||||
|         deletedAt: json["deleted_at"], |         deletedAt: json["deleted_at"], | ||||||
|     endedAt: json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, |         endedAt: | ||||||
|  |             json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, | ||||||
|         externalId: json["external_id"], |         externalId: json["external_id"], | ||||||
|         founderId: json["founder_id"], |         founderId: json["founder_id"], | ||||||
|         channelId: json["channel_id"], |         channelId: json["channel_id"], | ||||||
|   | |||||||
| @@ -43,11 +43,15 @@ class Message { | |||||||
|         content: json["content"], |         content: json["content"], | ||||||
|         metadata: json["metadata"], |         metadata: json["metadata"], | ||||||
|         type: json["type"], |         type: json["type"], | ||||||
|         attachments: List<Attachment>.from(json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? List.empty()), |         attachments: List<Attachment>.from( | ||||||
|  |             json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? | ||||||
|  |                 List.empty()), | ||||||
|         channel: Channel.fromJson(json["channel"]), |         channel: Channel.fromJson(json["channel"]), | ||||||
|         sender: Sender.fromJson(json["sender"]), |         sender: Sender.fromJson(json["sender"]), | ||||||
|         replyId: json["reply_id"], |         replyId: json["reply_id"], | ||||||
|         replyTo: json["reply_to"] != null ? Message.fromJson(json["reply_to"]) : null, |         replyTo: json["reply_to"] != null | ||||||
|  |             ? Message.fromJson(json["reply_to"]) | ||||||
|  |             : null, | ||||||
|         channelId: json["channel_id"], |         channelId: json["channel_id"], | ||||||
|         senderId: json["sender_id"], |         senderId: json["sender_id"], | ||||||
|       ); |       ); | ||||||
| @@ -60,7 +64,8 @@ class Message { | |||||||
|         "content": content, |         "content": content, | ||||||
|         "metadata": metadata, |         "metadata": metadata, | ||||||
|         "type": type, |         "type": type, | ||||||
|         "attachments": List<dynamic>.from(attachments?.map((x) => x.toJson()) ?? List.empty()), |         "attachments": List<dynamic>.from( | ||||||
|  |             attachments?.map((x) => x.toJson()) ?? List.empty()), | ||||||
|         "channel": channel?.toJson(), |         "channel": channel?.toJson(), | ||||||
|         "sender": sender.toJson(), |         "sender": sender.toJson(), | ||||||
|         "reply_id": replyId, |         "reply_id": replyId, | ||||||
|   | |||||||
| @@ -34,7 +34,9 @@ class Notification { | |||||||
|         deletedAt: json["deleted_at"], |         deletedAt: json["deleted_at"], | ||||||
|         subject: json["subject"], |         subject: json["subject"], | ||||||
|         content: json["content"], |         content: json["content"], | ||||||
|         links: json["links"] != null ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) : List.empty(), |         links: json["links"] != null | ||||||
|  |             ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) | ||||||
|  |             : List.empty(), | ||||||
|         isImportant: json["is_important"], |         isImportant: json["is_important"], | ||||||
|         isRealtime: json["is_realtime"], |         isRealtime: json["is_realtime"], | ||||||
|         readAt: json["read_at"], |         readAt: json["read_at"], | ||||||
| @@ -49,7 +51,9 @@ class Notification { | |||||||
|         "deleted_at": deletedAt, |         "deleted_at": deletedAt, | ||||||
|         "subject": subject, |         "subject": subject, | ||||||
|         "content": content, |         "content": content, | ||||||
|         "links": links != null ? List<dynamic>.from(links!.map((x) => x.toJson())) : List.empty(), |         "links": links != null | ||||||
|  |             ? List<dynamic>.from(links!.map((x) => x.toJson())) | ||||||
|  |             : List.empty(), | ||||||
|         "is_important": isImportant, |         "is_important": isImportant, | ||||||
|         "is_realtime": isRealtime, |         "is_realtime": isRealtime, | ||||||
|         "read_at": readAt, |         "read_at": readAt, | ||||||
|   | |||||||
| @@ -8,7 +8,8 @@ import 'package:solian/utils/service_url.dart'; | |||||||
| class AuthProvider extends ChangeNotifier { | class AuthProvider extends ChangeNotifier { | ||||||
|   AuthProvider(); |   AuthProvider(); | ||||||
|  |  | ||||||
|   final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe'); |   final deviceEndpoint = | ||||||
|  |       getRequestUri('passport', '/api/notifications/subscribe'); | ||||||
|   final tokenEndpoint = getRequestUri('passport', '/api/auth/token'); |   final tokenEndpoint = getRequestUri('passport', '/api/auth/token'); | ||||||
|   final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); |   final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); | ||||||
|   final redirectUrl = Uri.parse('solian://auth'); |   final redirectUrl = Uri.parse('solian://auth'); | ||||||
| @@ -29,8 +30,10 @@ class AuthProvider extends ChangeNotifier { | |||||||
|   Future<bool> loadClient() async { |   Future<bool> loadClient() async { | ||||||
|     if (await storage.containsKey(key: storageKey)) { |     if (await storage.containsKey(key: storageKey)) { | ||||||
|       try { |       try { | ||||||
|         final credentials = oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); |         final credentials = | ||||||
|         client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); |             oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); | ||||||
|  |         client = oauth2.Client(credentials, | ||||||
|  |             identifier: clientId, secret: clientSecret); | ||||||
|         await fetchProfiles(); |         await fetchProfiles(); | ||||||
|         return true; |         return true; | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
| @@ -42,7 +45,8 @@ class AuthProvider extends ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<oauth2.Client> createClient(BuildContext context, String username, String password) async { |   Future<oauth2.Client> createClient( | ||||||
|  |       BuildContext context, String username, String password) async { | ||||||
|     if (await loadClient()) { |     if (await loadClient()) { | ||||||
|       return client!; |       return client!; | ||||||
|     } |     } | ||||||
| @@ -68,15 +72,17 @@ class AuthProvider extends ChangeNotifier { | |||||||
|  |  | ||||||
|   Future<void> refreshToken() async { |   Future<void> refreshToken() async { | ||||||
|     if (client != null) { |     if (client != null) { | ||||||
|       final credentials = |       final credentials = await client!.credentials.refresh( | ||||||
|           await client!.credentials.refresh(identifier: clientId, secret: clientSecret, basicAuth: false); |           identifier: clientId, secret: clientSecret, basicAuth: false); | ||||||
|       client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); |       client = oauth2.Client(credentials, | ||||||
|  |           identifier: clientId, secret: clientSecret); | ||||||
|       storage.write(key: storageKey, value: credentials.toJson()); |       storage.write(key: storageKey, value: credentials.toJson()); | ||||||
|     } |     } | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> signin(BuildContext context, String username, String password) async { |   Future<void> signin( | ||||||
|  |       BuildContext context, String username, String password) async { | ||||||
|     client = await createClient(context, username, password); |     client = await createClient(context, username, password); | ||||||
|     storage.write(key: storageKey, value: client!.credentials.toJson()); |     storage.write(key: storageKey, value: client!.credentials.toJson()); | ||||||
|  |  | ||||||
| @@ -94,7 +100,10 @@ class AuthProvider extends ChangeNotifier { | |||||||
|       if (client == null) { |       if (client == null) { | ||||||
|         await loadClient(); |         await loadClient(); | ||||||
|       } |       } | ||||||
|       if (lastRefreshedAt == null || DateTime.now().subtract(const Duration(minutes: 3)).isAfter(lastRefreshedAt!)) { |       if (lastRefreshedAt == null || | ||||||
|  |           DateTime.now() | ||||||
|  |               .subtract(const Duration(minutes: 3)) | ||||||
|  |               .isAfter(lastRefreshedAt!)) { | ||||||
|         await refreshToken(); |         await refreshToken(); | ||||||
|         lastRefreshedAt = DateTime.now(); |         lastRefreshedAt = DateTime.now(); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -32,7 +32,9 @@ class ChatProvider extends ChangeNotifier { | |||||||
|       scheme: ori.scheme.replaceFirst('http', 'ws'), |       scheme: ori.scheme.replaceFirst('http', 'ws'), | ||||||
|       host: ori.host, |       host: ori.host, | ||||||
|       path: ori.path, |       path: ori.path, | ||||||
|       queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, |       queryParameters: { | ||||||
|  |         'tk': Uri.encodeComponent(auth.client!.credentials.accessToken) | ||||||
|  |       }, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     final channel = WebSocketChannel.connect(uri); |     final channel = WebSocketChannel.connect(uri); | ||||||
| @@ -41,7 +43,8 @@ class ChatProvider extends ChangeNotifier { | |||||||
|     return channel; |     return channel; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool handleCall(Call call, Channel channel, {Function? onUpdate, Function? onDispose}) { |   bool handleCall(Call call, Channel channel, | ||||||
|  |       {Function? onUpdate, Function? onDispose}) { | ||||||
|     if (this.call != null) return false; |     if (this.call != null) return false; | ||||||
|  |  | ||||||
|     this.call = ChatCallInstance( |     this.call = ChatCallInstance( | ||||||
| @@ -106,7 +109,8 @@ class ChatCallInstance { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   void init() { |   void init() { | ||||||
|     subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices); |     subscription = | ||||||
|  |         Hardware.instance.onDeviceChange.stream.listen(revertDevices); | ||||||
|     room = Room(); |     room = Room(); | ||||||
|     listener = room.createListener(); |     listener = room.createListener(); | ||||||
|     Hardware.instance.enumerateDevices().then(revertDevices); |     Hardware.instance.enumerateDevices().then(revertDevices); | ||||||
| @@ -114,7 +118,8 @@ class ChatCallInstance { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> checkPermissions() async { |   Future<void> checkPermissions() async { | ||||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; |     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|     await Permission.camera.request(); |     await Permission.camera.request(); | ||||||
|     await Permission.microphone.request(); |     await Permission.microphone.request(); | ||||||
| @@ -131,7 +136,8 @@ class ChatCallInstance { | |||||||
|       throw Exception("unauthorized"); |       throw Exception("unauthorized"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${channel.alias}/calls/ongoing/token'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${channel.alias}/calls/ongoing/token'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.post(uri); |     var res = await auth.client!.post(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -184,10 +190,12 @@ class ChatCallInstance { | |||||||
|             useiOSBroadcastExtension: true, |             useiOSBroadcastExtension: true, | ||||||
|             params: VideoParameters( |             params: VideoParameters( | ||||||
|               dimensions: VideoDimensionsPresets.h1080_169, |               dimensions: VideoDimensionsPresets.h1080_169, | ||||||
|               encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30), |               encoding: | ||||||
|  |                   VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: videoParameters), |           defaultCameraCaptureOptions: | ||||||
|  |               CameraCaptureOptions(maxFrameRate: 30, params: videoParameters), | ||||||
|         ), |         ), | ||||||
|         fastConnectOptions: FastConnectOptions( |         fastConnectOptions: FastConnectOptions( | ||||||
|           microphone: TrackOption(track: audioTrack), |           microphone: TrackOption(track: audioTrack), | ||||||
| @@ -220,7 +228,8 @@ class ChatCallInstance { | |||||||
|     room.addListener(onRoomDidUpdate); |     room.addListener(onRoomDidUpdate); | ||||||
|     setupRoomListeners(context); |     setupRoomListeners(context); | ||||||
|     sortParticipants(); |     sortParticipants(); | ||||||
|     WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish(context)); |     WidgetsBindingCompatible.instance | ||||||
|  |         ?.addPostFrameCallback((_) => autoPublish(context)); | ||||||
|  |  | ||||||
|     if (lkPlatformIsMobile()) { |     if (lkPlatformIsMobile()) { | ||||||
|       Hardware.instance.setSpeakerphoneOn(true); |       Hardware.instance.setSpeakerphoneOn(true); | ||||||
| @@ -295,7 +304,8 @@ class ChatCallInstance { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       // First joined people first |       // First joined people first | ||||||
|       return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch; |       return a.participant.joinedAt.millisecondsSinceEpoch - | ||||||
|  |           b.participant.joinedAt.millisecondsSinceEpoch; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     ParticipantTrack localTrack = ParticipantTrack( |     ParticipantTrack localTrack = ParticipantTrack( | ||||||
| @@ -304,7 +314,8 @@ class ChatCallInstance { | |||||||
|       isScreenShare: false, |       isScreenShare: false, | ||||||
|     ); |     ); | ||||||
|     if (room.localParticipant != null) { |     if (room.localParticipant != null) { | ||||||
|       final localParticipantTracks = room.localParticipant?.videoTrackPublications; |       final localParticipantTracks = | ||||||
|  |           room.localParticipant?.videoTrackPublications; | ||||||
|       if (localParticipantTracks != null) { |       if (localParticipantTracks != null) { | ||||||
|         for (var t in localParticipantTracks) { |         for (var t in localParticipantTracks) { | ||||||
|           localTrack.videoTrack = t.track; |           localTrack.videoTrack = t.track; | ||||||
| @@ -317,7 +328,8 @@ class ChatCallInstance { | |||||||
|     if (focusTrack == null) { |     if (focusTrack == null) { | ||||||
|       focusTrack = participantTracks.first; |       focusTrack = participantTracks.first; | ||||||
|     } else { |     } else { | ||||||
|       final idx = participantTracks.indexWhere((x) => focusTrack!.participant.sid == x.participant.sid); |       final idx = participantTracks | ||||||
|  |           .indexWhere((x) => focusTrack!.participant.sid == x.participant.sid); | ||||||
|       focusTrack = participantTracks[idx]; |       focusTrack = participantTracks[idx]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,8 @@ class NotifyProvider extends ChangeNotifier { | |||||||
|  |  | ||||||
|   List<model.Notification> notifications = List.empty(growable: true); |   List<model.Notification> notifications = List.empty(growable: true); | ||||||
|  |  | ||||||
|   final FlutterLocalNotificationsPlugin localNotify = FlutterLocalNotificationsPlugin(); |   final FlutterLocalNotificationsPlugin localNotify = | ||||||
|  |       FlutterLocalNotificationsPlugin(); | ||||||
|  |  | ||||||
|   NotifyProvider() { |   NotifyProvider() { | ||||||
|     initNotify(); |     initNotify(); | ||||||
| @@ -31,8 +32,10 @@ class NotifyProvider extends ChangeNotifier { | |||||||
|         DarwinNotificationCategory("general"), |         DarwinNotificationCategory("general"), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|     const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification'); |     const linuxSettings = | ||||||
|     const InitializationSettings initializationSettings = InitializationSettings( |         LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||||
|  |     const InitializationSettings initializationSettings = | ||||||
|  |         InitializationSettings( | ||||||
|       android: androidSettings, |       android: androidSettings, | ||||||
|       iOS: darwinSettings, |       iOS: darwinSettings, | ||||||
|       macOS: darwinSettings, |       macOS: darwinSettings, | ||||||
| @@ -43,7 +46,8 @@ class NotifyProvider extends ChangeNotifier { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> requestPermissions() async { |   Future<void> requestPermissions() async { | ||||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; |     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) | ||||||
|  |       return; | ||||||
|     await Permission.notification.request(); |     await Permission.notification.request(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -53,8 +57,11 @@ class NotifyProvider extends ChangeNotifier { | |||||||
|     var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25'); |     var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25'); | ||||||
|     var res = await auth.client!.get(uri); |     var res = await auth.client!.get(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); |       final result = | ||||||
|       notifications = result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? List.empty(growable: true); |           PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||||
|  |       notifications = | ||||||
|  |           result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? | ||||||
|  |               List.empty(growable: true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
| @@ -71,7 +78,9 @@ class NotifyProvider extends ChangeNotifier { | |||||||
|       scheme: ori.scheme.replaceFirst('http', 'ws'), |       scheme: ori.scheme.replaceFirst('http', 'ws'), | ||||||
|       host: ori.host, |       host: ori.host, | ||||||
|       path: ori.path, |       path: ori.path, | ||||||
|       queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, |       queryParameters: { | ||||||
|  |         'tk': Uri.encodeComponent(auth.client!.credentials.accessToken) | ||||||
|  |       }, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     final channel = WebSocketChannel.connect(uri); |     final channel = WebSocketChannel.connect(uri); | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import 'package:solian/models/channel.dart'; | |||||||
| import 'package:solian/models/post.dart'; | import 'package:solian/models/post.dart'; | ||||||
| import 'package:solian/screens/account.dart'; | import 'package:solian/screens/account.dart'; | ||||||
| import 'package:solian/screens/account/friend.dart'; | import 'package:solian/screens/account/friend.dart'; | ||||||
|  | import 'package:solian/screens/auth/signup.dart'; | ||||||
| import 'package:solian/screens/chat/call.dart'; | import 'package:solian/screens/chat/call.dart'; | ||||||
| import 'package:solian/screens/chat/chat.dart'; | import 'package:solian/screens/chat/chat.dart'; | ||||||
| import 'package:solian/screens/chat/index.dart'; | import 'package:solian/screens/chat/index.dart'; | ||||||
| @@ -15,7 +16,7 @@ import 'package:solian/screens/notification.dart'; | |||||||
| import 'package:solian/screens/posts/comment_editor.dart'; | import 'package:solian/screens/posts/comment_editor.dart'; | ||||||
| import 'package:solian/screens/posts/moment_editor.dart'; | import 'package:solian/screens/posts/moment_editor.dart'; | ||||||
| import 'package:solian/screens/posts/screen.dart'; | import 'package:solian/screens/posts/screen.dart'; | ||||||
| import 'package:solian/screens/signin.dart'; | import 'package:solian/screens/auth/signin.dart'; | ||||||
|  |  | ||||||
| final router = GoRouter( | final router = GoRouter( | ||||||
|   routes: [ |   routes: [ | ||||||
| @@ -37,12 +38,14 @@ final router = GoRouter( | |||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/chat/create', |       path: '/chat/create', | ||||||
|       name: 'chat.channel.editor', |       name: 'chat.channel.editor', | ||||||
|       builder: (context, state) => ChannelEditorScreen(editing: state.extra as Channel?), |       builder: (context, state) => | ||||||
|  |           ChannelEditorScreen(editing: state.extra as Channel?), | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/chat/c/:channel', |       path: '/chat/c/:channel', | ||||||
|       name: 'chat.channel', |       name: 'chat.channel', | ||||||
|       builder: (context, state) => ChatScreen(alias: state.pathParameters['channel'] as String), |       builder: (context, state) => | ||||||
|  |           ChatScreen(alias: state.pathParameters['channel'] as String), | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/chat/c/:channel/call', |       path: '/chat/c/:channel/call', | ||||||
| @@ -52,12 +55,14 @@ final router = GoRouter( | |||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/chat/c/:channel/manage', |       path: '/chat/c/:channel/manage', | ||||||
|       name: 'chat.channel.manage', |       name: 'chat.channel.manage', | ||||||
|       builder: (context, state) => ChatManageScreen(channel: state.extra as Channel), |       builder: (context, state) => | ||||||
|  |           ChatManageScreen(channel: state.extra as Channel), | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/chat/c/:channel/member', |       path: '/chat/c/:channel/member', | ||||||
|       name: 'chat.channel.member', |       name: 'chat.channel.member', | ||||||
|       builder: (context, state) => ChatMemberScreen(channel: state.extra as Channel), |       builder: (context, state) => | ||||||
|  |           ChatMemberScreen(channel: state.extra as Channel), | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/account', |       path: '/account', | ||||||
| @@ -67,14 +72,16 @@ final router = GoRouter( | |||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/posts/publish/moments', |       path: '/posts/publish/moments', | ||||||
|       name: 'posts.moments.editor', |       name: 'posts.moments.editor', | ||||||
|       builder: (context, state) => MomentEditorScreen(editing: state.extra as Post?), |       builder: (context, state) => | ||||||
|  |           MomentEditorScreen(editing: state.extra as Post?), | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/posts/publish/comments', |       path: '/posts/publish/comments', | ||||||
|       name: 'posts.comments.editor', |       name: 'posts.comments.editor', | ||||||
|       builder: (context, state) { |       builder: (context, state) { | ||||||
|         final args = state.extra as CommentPostArguments; |         final args = state.extra as CommentPostArguments; | ||||||
|         return CommentEditorScreen(editing: args.editing, related: args.related); |         return CommentEditorScreen( | ||||||
|  |             editing: args.editing, related: args.related); | ||||||
|       }, |       }, | ||||||
|     ), |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
| @@ -90,6 +97,11 @@ final router = GoRouter( | |||||||
|       name: 'auth.sign-in', |       name: 'auth.sign-in', | ||||||
|       builder: (context, state) => SignInScreen(), |       builder: (context, state) => SignInScreen(), | ||||||
|     ), |     ), | ||||||
|  |     GoRoute( | ||||||
|  |       path: '/auth/sign-up', | ||||||
|  |       name: 'auth.sign-up', | ||||||
|  |       builder: (context, state) => SignUpScreen(), | ||||||
|  |     ), | ||||||
|     GoRoute( |     GoRoute( | ||||||
|       path: '/account/friend', |       path: '/account/friend', | ||||||
|       name: 'account.friend', |       name: 'account.friend', | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ class _AccountScreenState extends State<AccountScreen> { | |||||||
|                     title: AppLocalizations.of(context)!.signUp, |                     title: AppLocalizations.of(context)!.signUp, | ||||||
|                     caption: AppLocalizations.of(context)!.signUpCaption, |                     caption: AppLocalizations.of(context)!.signUpCaption, | ||||||
|                     onTap: () { |                     onTap: () { | ||||||
|                       launchUrl(getRequestUri('passport', '/sign-up')); |                       router.pushNamed('auth.sign-up'); | ||||||
|                     }, |                     }, | ||||||
|                   ), |                   ), | ||||||
|                 ], |                 ], | ||||||
| @@ -131,7 +131,8 @@ class NameCard extends StatelessWidget { | |||||||
|             children: [ |             children: [ | ||||||
|               FutureBuilder( |               FutureBuilder( | ||||||
|                 future: renderAvatar(context), |                 future: renderAvatar(context), | ||||||
|                 builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { |                 builder: | ||||||
|  |                     (BuildContext context, AsyncSnapshot<Widget> snapshot) { | ||||||
|                   if (snapshot.hasData) { |                   if (snapshot.hasData) { | ||||||
|                     return snapshot.data!; |                     return snapshot.data!; | ||||||
|                   } else { |                   } else { | ||||||
| @@ -142,7 +143,8 @@ class NameCard extends StatelessWidget { | |||||||
|               const SizedBox(width: 20), |               const SizedBox(width: 20), | ||||||
|               FutureBuilder( |               FutureBuilder( | ||||||
|                 future: renderLabel(context), |                 future: renderLabel(context), | ||||||
|                 builder: (BuildContext context, AsyncSnapshot<Column> snapshot) { |                 builder: | ||||||
|  |                     (BuildContext context, AsyncSnapshot<Column> snapshot) { | ||||||
|                   if (snapshot.hasData) { |                   if (snapshot.hasData) { | ||||||
|                     return snapshot.data!; |                     return snapshot.data!; | ||||||
|                   } else { |                   } else { | ||||||
| @@ -164,7 +166,12 @@ class ActionCard extends StatelessWidget { | |||||||
|   final String caption; |   final String caption; | ||||||
|   final Function onTap; |   final Function onTap; | ||||||
|  |  | ||||||
|   const ActionCard({super.key, required this.onTap, required this.title, required this.caption, required this.icon}); |   const ActionCard( | ||||||
|  |       {super.key, | ||||||
|  |       required this.onTap, | ||||||
|  |       required this.title, | ||||||
|  |       required this.caption, | ||||||
|  |       required this.icon}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|   | |||||||
| @@ -120,14 +120,16 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|                   border: const OutlineInputBorder(), |                   border: const OutlineInputBorder(), | ||||||
|                   labelText: AppLocalizations.of(context)!.username, |                   labelText: AppLocalizations.of(context)!.username, | ||||||
|                 ), |                 ), | ||||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|           actions: <Widget>[ |           actions: <Widget>[ | ||||||
|             TextButton( |             TextButton( | ||||||
|               style: TextButton.styleFrom( |               style: TextButton.styleFrom( | ||||||
|                 foregroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), |                 foregroundColor: | ||||||
|  |                     Theme.of(context).colorScheme.onSurface.withOpacity(0.8), | ||||||
|               ), |               ), | ||||||
|               onPressed: () => Navigator.pop(context), |               onPressed: () => Navigator.pop(context), | ||||||
|               child: Text(AppLocalizations.of(context)!.cancel), |               child: Text(AppLocalizations.of(context)!.cancel), | ||||||
| @@ -155,7 +157,8 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|   DismissDirection getDismissDirection(Friendship relation) { |   DismissDirection getDismissDirection(Friendship relation) { | ||||||
|     if (relation.status == 2) return DismissDirection.endToStart; |     if (relation.status == 2) return DismissDirection.endToStart; | ||||||
|     if (relation.status == 1) return DismissDirection.startToEnd; |     if (relation.status == 1) return DismissDirection.startToEnd; | ||||||
|     if (relation.status == 0 && relation.relatedId != _selfId) return DismissDirection.startToEnd; |     if (relation.status == 0 && relation.relatedId != _selfId) | ||||||
|  |       return DismissDirection.startToEnd; | ||||||
|     return DismissDirection.horizontal; |     return DismissDirection.horizontal; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -220,12 +223,18 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|         child: CustomScrollView( |         child: CustomScrollView( | ||||||
|           slivers: [ |           slivers: [ | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |               child: _isSubmitting | ||||||
|  |                   ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |                   : Container(), | ||||||
|             ), |             ), | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: Container( |               child: Container( | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), |                 padding: | ||||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), |                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||||
|  |                 color: Theme.of(context) | ||||||
|  |                     .colorScheme | ||||||
|  |                     .surfaceVariant | ||||||
|  |                     .withOpacity(0.8), | ||||||
|                 child: Text(AppLocalizations.of(context)!.friendPending), |                 child: Text(AppLocalizations.of(context)!.friendPending), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -235,8 +244,12 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|             ), |             ), | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: Container( |               child: Container( | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), |                 padding: | ||||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), |                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||||
|  |                 color: Theme.of(context) | ||||||
|  |                     .colorScheme | ||||||
|  |                     .surfaceVariant | ||||||
|  |                     .withOpacity(0.8), | ||||||
|                 child: Text(AppLocalizations.of(context)!.friendActive), |                 child: Text(AppLocalizations.of(context)!.friendActive), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -246,8 +259,12 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|             ), |             ), | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: Container( |               child: Container( | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), |                 padding: | ||||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), |                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||||
|  |                 color: Theme.of(context) | ||||||
|  |                     .colorScheme | ||||||
|  |                     .surfaceVariant | ||||||
|  |                     .withOpacity(0.8), | ||||||
|                 child: Text(AppLocalizations.of(context)!.friendBlocked), |                 child: Text(AppLocalizations.of(context)!.friendBlocked), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -260,7 +277,10 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|                 decoration: BoxDecoration( |                 decoration: BoxDecoration( | ||||||
|                   border: Border( |                   border: Border( | ||||||
|                       top: BorderSide( |                       top: BorderSide( | ||||||
|                     color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), |                     color: Theme.of(context) | ||||||
|  |                         .colorScheme | ||||||
|  |                         .surfaceVariant | ||||||
|  |                         .withOpacity(0.8), | ||||||
|                     width: 0.3, |                     width: 0.3, | ||||||
|                   )), |                   )), | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
							
								
								
									
										121
									
								
								lib/screens/auth/signin.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								lib/screens/auth/signin.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||||
|  | import 'package:provider/provider.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:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
|  | class SignInScreen extends StatelessWidget { | ||||||
|  |   final _usernameController = TextEditingController(); | ||||||
|  |   final _passwordController = TextEditingController(); | ||||||
|  |  | ||||||
|  |   SignInScreen({super.key}); | ||||||
|  |  | ||||||
|  |   void performSignIn(BuildContext context) { | ||||||
|  |     final auth = context.read<AuthProvider>(); | ||||||
|  |  | ||||||
|  |     final username = _usernameController.value.text; | ||||||
|  |     final password = _passwordController.value.text; | ||||||
|  |     if (username.isEmpty || password.isEmpty) return; | ||||||
|  |     auth.signin(context, username, password).then((_) { | ||||||
|  |       router.pop(true); | ||||||
|  |     }).catchError((e) { | ||||||
|  |       List<String> messages = e.toString().split('\n'); | ||||||
|  |       if (messages.last.contains("risk")) { | ||||||
|  |         final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last); | ||||||
|  |         if (ticketId == null) { | ||||||
|  |           context.showErrorDialog( | ||||||
|  |               "requested to multi-factor authenticate, but the ticket id was not found"); | ||||||
|  |         } | ||||||
|  |         showDialog( | ||||||
|  |           context: context, | ||||||
|  |           builder: (context) { | ||||||
|  |             return AlertDialog( | ||||||
|  |               title: Text(AppLocalizations.of(context)!.riskDetection), | ||||||
|  |               content: Text(AppLocalizations.of(context)!.signInRiskDetected), | ||||||
|  |               actions: [ | ||||||
|  |                 TextButton( | ||||||
|  |                   child: Text(AppLocalizations.of(context)!.next), | ||||||
|  |                   onPressed: () { | ||||||
|  |                     launchUrlString( | ||||||
|  |                       getRequestUri( | ||||||
|  |                               'passport', '/mfa?ticket=${ticketId!.group(1)}') | ||||||
|  |                           .toString(), | ||||||
|  |                     ); | ||||||
|  |                     if (Navigator.canPop(context)) { | ||||||
|  |                       Navigator.pop(context); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|  |                 ) | ||||||
|  |               ], | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||||||
|  |           content: Text(messages.last), | ||||||
|  |         )); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return IndentWrapper( | ||||||
|  |       title: AppLocalizations.of(context)!.signIn, | ||||||
|  |       hideDrawer: true, | ||||||
|  |       child: Center( | ||||||
|  |         child: Container( | ||||||
|  |           width: MediaQuery.of(context).size.width * 0.6, | ||||||
|  |           constraints: const BoxConstraints(maxWidth: 360), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: [ | ||||||
|  |               Container( | ||||||
|  |                 padding: const EdgeInsets.symmetric(vertical: 16), | ||||||
|  |                 child: Image.asset('assets/logo.png', width: 72, height: 72), | ||||||
|  |               ), | ||||||
|  |               TextField( | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 controller: _usernameController, | ||||||
|  |                 autofillHints: const [AutofillHints.username], | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.username, | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 12), | ||||||
|  |               TextField( | ||||||
|  |                 obscureText: true, | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 autofillHints: const [AutofillHints.password], | ||||||
|  |                 controller: _passwordController, | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.password, | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                 onSubmitted: (_) => performSignIn(context), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 16), | ||||||
|  |               ElevatedButton( | ||||||
|  |                 child: Text(AppLocalizations.of(context)!.signIn), | ||||||
|  |                 onPressed: () => performSignIn(context), | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								lib/screens/auth/signup.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								lib/screens/auth/signup.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:solian/providers/auth.dart'; | ||||||
|  | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||||
|  | import 'package:solian/widgets/indent_wrapper.dart'; | ||||||
|  |  | ||||||
|  | class SignUpScreen extends StatelessWidget { | ||||||
|  |   final _emailController = TextEditingController(); | ||||||
|  |   final _usernameController = TextEditingController(); | ||||||
|  |   final _nicknameController = TextEditingController(); | ||||||
|  |   final _passwordController = TextEditingController(); | ||||||
|  |  | ||||||
|  |   SignUpScreen({super.key}); | ||||||
|  |  | ||||||
|  |   void performSignIn(BuildContext context) { | ||||||
|  |     final auth = context.read<AuthProvider>(); | ||||||
|  |  | ||||||
|  |     final email = _emailController.value.text; | ||||||
|  |     final username = _usernameController.value.text; | ||||||
|  |     final nickname = _passwordController.value.text; | ||||||
|  |     final password = _passwordController.value.text; | ||||||
|  |     if (email.isEmpty || | ||||||
|  |         username.isEmpty || | ||||||
|  |         nickname.isEmpty || | ||||||
|  |         password.isEmpty) return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return IndentWrapper( | ||||||
|  |       title: AppLocalizations.of(context)!.signUp, | ||||||
|  |       hideDrawer: true, | ||||||
|  |       child: Center( | ||||||
|  |         child: Container( | ||||||
|  |           width: MediaQuery.of(context).size.width * 0.6, | ||||||
|  |           constraints: const BoxConstraints(maxWidth: 360), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: [ | ||||||
|  |               Container( | ||||||
|  |                 padding: const EdgeInsets.symmetric(vertical: 16), | ||||||
|  |                 child: Image.asset('assets/logo.png', width: 72, height: 72), | ||||||
|  |               ), | ||||||
|  |               TextField( | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 controller: _usernameController, | ||||||
|  |                 autofillHints: const [AutofillHints.username], | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.username, | ||||||
|  |                   prefixText: '@', | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 12), | ||||||
|  |               TextField( | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 controller: _nicknameController, | ||||||
|  |                 autofillHints: const [AutofillHints.nickname], | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.nickname, | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 12), | ||||||
|  |               TextField( | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 controller: _emailController, | ||||||
|  |                 autofillHints: const [AutofillHints.email], | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.email, | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 12), | ||||||
|  |               TextField( | ||||||
|  |                 obscureText: true, | ||||||
|  |                 autocorrect: false, | ||||||
|  |                 enableSuggestions: false, | ||||||
|  |                 autofillHints: const [AutofillHints.password], | ||||||
|  |                 controller: _passwordController, | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   isDense: true, | ||||||
|  |                   border: const OutlineInputBorder(), | ||||||
|  |                   labelText: AppLocalizations.of(context)!.password, | ||||||
|  |                 ), | ||||||
|  |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                 onSubmitted: (_) => performSignIn(context), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 16), | ||||||
|  |               ElevatedButton( | ||||||
|  |                 child: Text(AppLocalizations.of(context)!.signUp), | ||||||
|  |                 onPressed: () => performSignIn(context), | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -92,14 +92,16 @@ class _ChatCallState extends State<ChatCall> { | |||||||
|                     itemCount: math.max(0, _call.participantTracks.length), |                     itemCount: math.max(0, _call.participantTracks.length), | ||||||
|                     itemBuilder: (BuildContext context, int index) { |                     itemBuilder: (BuildContext context, int index) { | ||||||
|                       final track = _call.participantTracks[index]; |                       final track = _call.participantTracks[index]; | ||||||
|                       if (track.participant.sid == _call.focusTrack?.participant.sid) { |                       if (track.participant.sid == | ||||||
|  |                           _call.focusTrack?.participant.sid) { | ||||||
|                         return Container(); |                         return Container(); | ||||||
|                       } |                       } | ||||||
|  |  | ||||||
|                       return Padding( |                       return Padding( | ||||||
|                         padding: const EdgeInsets.only(top: 8, left: 8), |                         padding: const EdgeInsets.only(top: 8, left: 8), | ||||||
|                         child: ClipRRect( |                         child: ClipRRect( | ||||||
|                           borderRadius: const BorderRadius.all(Radius.circular(8)), |                           borderRadius: | ||||||
|  |                               const BorderRadius.all(Radius.circular(8)), | ||||||
|                           child: InteractiveParticipantWidget( |                           child: InteractiveParticipantWidget( | ||||||
|                             isFixed: true, |                             isFixed: true, | ||||||
|                             width: 120, |                             width: 120, | ||||||
| @@ -107,7 +109,8 @@ class _ChatCallState extends State<ChatCall> { | |||||||
|                             color: Theme.of(context).cardColor, |                             color: Theme.of(context).cardColor, | ||||||
|                             participant: track, |                             participant: track, | ||||||
|                             onTap: () { |                             onTap: () { | ||||||
|                               if (track.participant.sid != _call.focusTrack?.participant.sid) { |                               if (track.participant.sid != | ||||||
|  |                                   _call.focusTrack?.participant.sid) { | ||||||
|                                 _call.changeFocusTrack(track); |                                 _call.changeFocusTrack(track); | ||||||
|                               } |                               } | ||||||
|                             }, |                             }, | ||||||
|   | |||||||
| @@ -113,10 +113,13 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | |||||||
|           constraints: const BoxConstraints(maxWidth: 640), |           constraints: const BoxConstraints(maxWidth: 640), | ||||||
|           child: Column( |           child: Column( | ||||||
|             children: [ |             children: [ | ||||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |               _isSubmitting | ||||||
|  |                   ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |                   : Container(), | ||||||
|               ListTile( |               ListTile( | ||||||
|                 title: Text(AppLocalizations.of(context)!.chatChannelUsage), |                 title: Text(AppLocalizations.of(context)!.chatChannelUsage), | ||||||
|                 subtitle: Text(AppLocalizations.of(context)!.chatChannelUsageCaption), |                 subtitle: | ||||||
|  |                     Text(AppLocalizations.of(context)!.chatChannelUsageCaption), | ||||||
|                 leading: const CircleAvatar( |                 leading: const CircleAvatar( | ||||||
|                   backgroundColor: Colors.teal, |                   backgroundColor: Colors.teal, | ||||||
|                   child: Icon(Icons.tag, color: Colors.white), |                   child: Icon(Icons.tag, color: Colors.white), | ||||||
| @@ -124,7 +127,8 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | |||||||
|               ), |               ), | ||||||
|               const Divider(thickness: 0.3), |               const Divider(thickness: 0.3), | ||||||
|               Container( |               Container( | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), |                 padding: | ||||||
|  |                     const EdgeInsets.symmetric(horizontal: 16, vertical: 2), | ||||||
|                 child: Row( |                 child: Row( | ||||||
|                   children: [ |                   children: [ | ||||||
|                     Expanded( |                     Expanded( | ||||||
| @@ -132,15 +136,18 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | |||||||
|                         autofocus: true, |                         autofocus: true, | ||||||
|                         controller: _aliasController, |                         controller: _aliasController, | ||||||
|                         decoration: InputDecoration.collapsed( |                         decoration: InputDecoration.collapsed( | ||||||
|                           hintText: AppLocalizations.of(context)!.chatChannelAliasLabel, |                           hintText: AppLocalizations.of(context)! | ||||||
|  |                               .chatChannelAliasLabel, | ||||||
|                         ), |                         ), | ||||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                         onTapOutside: (_) => | ||||||
|  |                             FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     TextButton( |                     TextButton( | ||||||
|                       style: TextButton.styleFrom( |                       style: TextButton.styleFrom( | ||||||
|                         shape: const CircleBorder(), |                         shape: const CircleBorder(), | ||||||
|                         visualDensity: const VisualDensity(horizontal: -2, vertical: -2), |                         visualDensity: | ||||||
|  |                             const VisualDensity(horizontal: -2, vertical: -2), | ||||||
|                       ), |                       ), | ||||||
|                       onPressed: () => randomizeAlias(), |                       onPressed: () => randomizeAlias(), | ||||||
|                       child: const Icon(Icons.refresh), |                       child: const Icon(Icons.refresh), | ||||||
| @@ -150,20 +157,24 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | |||||||
|               ), |               ), | ||||||
|               const Divider(thickness: 0.3), |               const Divider(thickness: 0.3), | ||||||
|               Container( |               Container( | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |                 padding: | ||||||
|  |                     const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||||
|                 child: TextField( |                 child: TextField( | ||||||
|                   autocorrect: true, |                   autocorrect: true, | ||||||
|                   controller: _nameController, |                   controller: _nameController, | ||||||
|                   decoration: InputDecoration.collapsed( |                   decoration: InputDecoration.collapsed( | ||||||
|                     hintText: AppLocalizations.of(context)!.chatChannelNameLabel, |                     hintText: | ||||||
|  |                         AppLocalizations.of(context)!.chatChannelNameLabel, | ||||||
|                   ), |                   ), | ||||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                   onTapOutside: (_) => | ||||||
|  |                       FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|               const Divider(thickness: 0.3), |               const Divider(thickness: 0.3), | ||||||
|               Expanded( |               Expanded( | ||||||
|                 child: Container( |                 child: Container( | ||||||
|                   padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |                   padding: | ||||||
|  |                       const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||||
|                   child: TextField( |                   child: TextField( | ||||||
|                     minLines: 5, |                     minLines: 5, | ||||||
|                     maxLines: null, |                     maxLines: null, | ||||||
| @@ -171,9 +182,11 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | |||||||
|                     keyboardType: TextInputType.multiline, |                     keyboardType: TextInputType.multiline, | ||||||
|                     controller: _descriptionController, |                     controller: _descriptionController, | ||||||
|                     decoration: InputDecoration.collapsed( |                     decoration: InputDecoration.collapsed( | ||||||
|                       hintText: AppLocalizations.of(context)!.chatChannelDescriptionLabel, |                       hintText: AppLocalizations.of(context)! | ||||||
|  |                           .chatChannelDescriptionLabel, | ||||||
|                     ), |                     ), | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                     onTapOutside: (_) => | ||||||
|  |                         FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|   | |||||||
| @@ -36,7 +36,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|  |  | ||||||
|     _selfId = prof['id']; |     _selfId = prof['id']; | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/members'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.channel.alias}/members'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.get(uri); |     var res = await auth.client!.get(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -59,7 +60,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/kick'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.channel.alias}/kick'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.post( |     var res = await auth.client!.post( | ||||||
|       uri, |       uri, | ||||||
| @@ -89,7 +91,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/invite'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.channel.alias}/invite'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.post( |     var res = await auth.client!.post( | ||||||
|       uri, |       uri, | ||||||
| @@ -153,7 +156,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|         child: CustomScrollView( |         child: CustomScrollView( | ||||||
|           slivers: [ |           slivers: [ | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |               child: _isSubmitting | ||||||
|  |                   ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |                   : Container(), | ||||||
|             ), |             ), | ||||||
|             SliverList.builder( |             SliverList.builder( | ||||||
|               itemCount: _members.length, |               itemCount: _members.length, | ||||||
| @@ -164,7 +169,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|  |  | ||||||
|                 return Dismissible( |                 return Dismissible( | ||||||
|                   key: Key(randomId.toString()), |                   key: Key(randomId.toString()), | ||||||
|                   direction: getKickable(element) ? DismissDirection.startToEnd : DismissDirection.none, |                   direction: getKickable(element) | ||||||
|  |                       ? DismissDirection.startToEnd | ||||||
|  |                       : DismissDirection.none, | ||||||
|                   background: Container( |                   background: Container( | ||||||
|                     color: Colors.red, |                     color: Colors.red, | ||||||
|                     padding: const EdgeInsets.symmetric(horizontal: 20), |                     padding: const EdgeInsets.symmetric(horizontal: 20), | ||||||
| @@ -172,7 +179,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | |||||||
|                     child: const Icon(Icons.remove, color: Colors.white), |                     child: const Icon(Icons.remove, color: Colors.white), | ||||||
|                   ), |                   ), | ||||||
|                   child: ListTile( |                   child: ListTile( | ||||||
|                     leading: AccountAvatar(source: element.account.avatar, direct: true), |                     leading: AccountAvatar( | ||||||
|  |                         source: element.account.avatar, direct: true), | ||||||
|                     title: Text(element.account.nick), |                     title: Text(element.account.nick), | ||||||
|                     subtitle: Text(element.account.name), |                     subtitle: Text(element.account.name), | ||||||
|                   ), |                   ), | ||||||
|   | |||||||
| @@ -34,7 +34,8 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|   Call? _ongoingCall; |   Call? _ongoingCall; | ||||||
|   Channel? _channelMeta; |   Channel? _channelMeta; | ||||||
|  |  | ||||||
|   final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); |   final PagingController<int, Message> _pagingController = | ||||||
|  |       PagingController(firstPageKey: 0); | ||||||
|  |  | ||||||
|   final http.Client _client = http.Client(); |   final http.Client _client = http.Client(); | ||||||
|  |  | ||||||
| @@ -53,7 +54,8 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<Call?> fetchCall() async { |   Future<Call?> fetchCall() async { | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.alias}/calls/ongoing'); | ||||||
|     var res = await _client.get(uri); |     var res = await _client.get(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)); |       final result = jsonDecode(utf8.decode(res.bodyBytes)); | ||||||
| @@ -82,8 +84,10 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|  |  | ||||||
|     var res = await auth.client!.get(uri); |     var res = await auth.client!.get(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); |       final result = | ||||||
|       final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); |           PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||||
|  |       final items = | ||||||
|  |           result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); | ||||||
|       final isLastPage = (result.count - pageKey) < take; |       final isLastPage = (result.count - pageKey) < take; | ||||||
|       if (isLastPage || result.data == null) { |       if (isLastPage || result.data == null) { | ||||||
|         _pagingController.appendLastPage(items); |         _pagingController.appendLastPage(items); | ||||||
| @@ -97,7 +101,7 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool getMessageMergeable(Message? a, Message? b) { |   bool getMessageMergeable(Message? a, Message? b) { | ||||||
|     if (a?.replyTo != null || b?.replyTo != null) return false; |     if (a?.replyTo != null) return false; | ||||||
|     if (a == null || b == null) return false; |     if (a == null || b == null) return false; | ||||||
|     if (a.senderId != b.senderId) return false; |     if (a.senderId != b.senderId) return false; | ||||||
|     return a.createdAt.difference(b.createdAt).inMinutes <= 5; |     return a.createdAt.difference(b.createdAt).inMinutes <= 5; | ||||||
| @@ -111,13 +115,16 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|  |  | ||||||
|   void updateMessage(Message item) { |   void updateMessage(Message item) { | ||||||
|     setState(() { |     setState(() { | ||||||
|       _pagingController.itemList = _pagingController.itemList?.map((x) => x.id == item.id ? item : x).toList(); |       _pagingController.itemList = _pagingController.itemList | ||||||
|  |           ?.map((x) => x.id == item.id ? item : x) | ||||||
|  |           .toList(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void deleteMessage(Message item) { |   void deleteMessage(Message item) { | ||||||
|     setState(() { |     setState(() { | ||||||
|       _pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList(); |       _pagingController.itemList = | ||||||
|  |           _pagingController.itemList?.where((x) => x.id != item.id).toList(); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -147,7 +154,8 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|       fetchCall(); |       fetchCall(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); |     _pagingController | ||||||
|  |         .addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); | ||||||
|  |  | ||||||
|     super.initState(); |     super.initState(); | ||||||
|   } |   } | ||||||
| @@ -157,10 +165,12 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|     Widget chatHistoryBuilder(context, item, index) { |     Widget chatHistoryBuilder(context, item, index) { | ||||||
|       bool isMerged = false, hasMerged = false; |       bool isMerged = false, hasMerged = false; | ||||||
|       if (index > 0) { |       if (index > 0) { | ||||||
|         hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); |         hasMerged = | ||||||
|  |             getMessageMergeable(_pagingController.itemList?[index - 1], item); | ||||||
|       } |       } | ||||||
|       if (index + 1 < (_pagingController.itemList?.length ?? 0)) { |       if (index + 1 < (_pagingController.itemList?.length ?? 0)) { | ||||||
|         isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); |         isMerged = | ||||||
|  |             getMessageMergeable(item, _pagingController.itemList?[index + 1]); | ||||||
|       } |       } | ||||||
|       return InkWell( |       return InkWell( | ||||||
|         child: Container( |         child: Container( | ||||||
| @@ -183,7 +193,8 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|     final callBanner = MaterialBanner( |     final callBanner = MaterialBanner( | ||||||
|       padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), |       padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), | ||||||
|       leading: const Icon(Icons.call_received), |       leading: const Icon(Icons.call_received), | ||||||
|       backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), |       backgroundColor: | ||||||
|  |           Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), | ||||||
|       dividerColor: const Color.fromARGB(1, 0, 0, 0), |       dividerColor: const Color.fromARGB(1, 0, 0, 0), | ||||||
|       content: Text(AppLocalizations.of(context)!.chatCallOngoing), |       content: Text(AppLocalizations.of(context)!.chatCallOngoing), | ||||||
|       actions: [ |       actions: [ | ||||||
| @@ -205,8 +216,12 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|       title: _channelMeta?.name ?? "Loading...", |       title: _channelMeta?.name ?? "Loading...", | ||||||
|       appBarActions: _channelMeta != null |       appBarActions: _channelMeta != null | ||||||
|           ? [ |           ? [ | ||||||
|               ChannelCallAction(call: _ongoingCall, channel: _channelMeta!, onUpdate: () => fetchMetadata()), |               ChannelCallAction( | ||||||
|               ChannelManageAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()), |                   call: _ongoingCall, | ||||||
|  |                   channel: _channelMeta!, | ||||||
|  |                   onUpdate: () => fetchMetadata()), | ||||||
|  |               ChannelManageAction( | ||||||
|  |                   channel: _channelMeta!, onUpdate: () => fetchMetadata()), | ||||||
|             ] |             ] | ||||||
|           : [], |           : [], | ||||||
|       child: FutureBuilder( |       child: FutureBuilder( | ||||||
| @@ -243,7 +258,9 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|                     ), |                     ), | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
|                 _ongoingCall != null ? callBanner.animate().slideY() : Container(), |                 _ongoingCall != null | ||||||
|  |                     ? callBanner.animate().slideY() | ||||||
|  |                     : Container(), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|             onInsertMessage: (message) => addMessage(message), |             onInsertMessage: (message) => addMessage(message), | ||||||
|   | |||||||
| @@ -53,7 +53,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | |||||||
|         leading: const Icon(Icons.settings), |         leading: const Icon(Icons.settings), | ||||||
|         title: Text(AppLocalizations.of(context)!.settings), |         title: Text(AppLocalizations.of(context)!.settings), | ||||||
|         onTap: () async { |         onTap: () async { | ||||||
|           router.pushNamed('chat.channel.editor', extra: widget.channel).then((did) { |           router | ||||||
|  |               .pushNamed('chat.channel.editor', extra: widget.channel) | ||||||
|  |               .then((did) { | ||||||
|             if (did == true) { |             if (did == true) { | ||||||
|               if (router.canPop()) router.pop('refresh'); |               if (router.canPop()) router.pop('refresh'); | ||||||
|             } |             } | ||||||
| @@ -79,9 +81,13 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | |||||||
|                 ), |                 ), | ||||||
|                 const SizedBox(width: 16), |                 const SizedBox(width: 16), | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ |                   child: Column( | ||||||
|                     Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge), |                       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                     Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall), |                       children: [ | ||||||
|  |                         Text(widget.channel.name, | ||||||
|  |                             style: Theme.of(context).textTheme.bodyLarge), | ||||||
|  |                         Text(widget.channel.description, | ||||||
|  |                             style: Theme.of(context).textTheme.bodySmall), | ||||||
|                       ]), |                       ]), | ||||||
|                 ) |                 ) | ||||||
|               ], |               ], | ||||||
| @@ -110,8 +116,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | |||||||
|                 ...(_isOwned ? authorizedItems : List.empty()), |                 ...(_isOwned ? authorizedItems : List.empty()), | ||||||
|                 const Divider(thickness: 0.3), |                 const Divider(thickness: 0.3), | ||||||
|                 ListTile( |                 ListTile( | ||||||
|                   leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app), |                   leading: _isOwned | ||||||
|                   title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit), |                       ? const Icon(Icons.delete) | ||||||
|  |                       : const Icon(Icons.exit_to_app), | ||||||
|  |                   title: Text(_isOwned | ||||||
|  |                       ? AppLocalizations.of(context)!.delete | ||||||
|  |                       : AppLocalizations.of(context)!.exit), | ||||||
|                   onTap: () => promptLeaveChannel(), |                   onTap: () => promptLeaveChannel(), | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|   | |||||||
| @@ -22,7 +22,8 @@ class ExploreScreen extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _ExploreScreenState extends State<ExploreScreen> { | class _ExploreScreenState extends State<ExploreScreen> { | ||||||
|   final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0); |   final PagingController<int, Post> _pagingController = | ||||||
|  |       PagingController(firstPageKey: 0); | ||||||
|  |  | ||||||
|   final http.Client _client = http.Client(); |   final http.Client _client = http.Client(); | ||||||
|  |  | ||||||
| @@ -30,12 +31,15 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|     final offset = pageKey; |     final offset = pageKey; | ||||||
|     const take = 5; |     const take = 5; | ||||||
|  |  | ||||||
|     var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset'); |     var uri = | ||||||
|  |         getRequestUri('interactive', '/api/feed?take=$take&offset=$offset'); | ||||||
|  |  | ||||||
|     var res = await _client.get(uri); |     var res = await _client.get(uri); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); |       final result = | ||||||
|       final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty(); |           PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||||
|  |       final items = | ||||||
|  |           result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty(); | ||||||
|       final isLastPage = (result.count - pageKey) < take; |       final isLastPage = (result.count - pageKey) < take; | ||||||
|       if (isLastPage || result.data == null) { |       if (isLastPage || result.data == null) { | ||||||
|         _pagingController.appendLastPage(items); |         _pagingController.appendLastPage(items); | ||||||
|   | |||||||
| @@ -41,7 +41,8 @@ class _NotificationScreenState extends State<NotificationScreen> { | |||||||
|                       child: ListTile( |                       child: ListTile( | ||||||
|                         leading: const Icon(Icons.check), |                         leading: const Icon(Icons.check), | ||||||
|                         title: Text(AppLocalizations.of(context)!.notifyDone), |                         title: Text(AppLocalizations.of(context)!.notifyDone), | ||||||
|                         subtitle: Text(AppLocalizations.of(context)!.notifyDoneCaption), |                         subtitle: Text( | ||||||
|  |                             AppLocalizations.of(context)!.notifyDoneCaption), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                   ) |                   ) | ||||||
| @@ -78,7 +79,8 @@ class NotificationItem extends StatelessWidget { | |||||||
|   final model.Notification item; |   final model.Notification item; | ||||||
|   final void Function()? onDismiss; |   final void Function()? onDismiss; | ||||||
|  |  | ||||||
|   const NotificationItem({super.key, required this.index, required this.item, this.onDismiss}); |   const NotificationItem( | ||||||
|  |       {super.key, required this.index, required this.item, this.onDismiss}); | ||||||
|  |  | ||||||
|   bool hasLinks() => item.links != null && item.links!.isNotEmpty; |   bool hasLinks() => item.links != null && item.links!.isNotEmpty; | ||||||
|  |  | ||||||
| @@ -92,7 +94,8 @@ class NotificationItem extends StatelessWidget { | |||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           children: [ |           children: [ | ||||||
|             Padding( |             Padding( | ||||||
|               padding: const EdgeInsets.only(left: 16, right: 16, top: 34, bottom: 12), |               padding: const EdgeInsets.only( | ||||||
|  |                   left: 16, right: 16, top: 34, bottom: 12), | ||||||
|               child: Text( |               child: Text( | ||||||
|                 "Links", |                 "Links", | ||||||
|                 style: Theme.of(context).textTheme.headlineSmall, |                 style: Theme.of(context).textTheme.headlineSmall, | ||||||
| @@ -121,7 +124,8 @@ class NotificationItem extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> markAsRead(model.Notification element, BuildContext context) async { |   Future<void> markAsRead( | ||||||
|  |       model.Notification element, BuildContext context) async { | ||||||
|     if (element.isRealtime) return; |     if (element.isRealtime) return; | ||||||
|  |  | ||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|   | |||||||
| @@ -129,12 +129,11 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | |||||||
|           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), |           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|       child: Center( |  | ||||||
|         child: Container( |  | ||||||
|           constraints: const BoxConstraints(maxWidth: 640), |  | ||||||
|       child: Column( |       child: Column( | ||||||
|         children: [ |         children: [ | ||||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |           _isSubmitting | ||||||
|  |               ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |               : Container(), | ||||||
|           FutureBuilder( |           FutureBuilder( | ||||||
|             future: auth.getProfiles(), |             future: auth.getProfiles(), | ||||||
|             builder: (context, snapshot) { |             builder: (context, snapshot) { | ||||||
| @@ -166,9 +165,11 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | |||||||
|                 keyboardType: TextInputType.multiline, |                 keyboardType: TextInputType.multiline, | ||||||
|                 controller: _textController, |                 controller: _textController, | ||||||
|                 decoration: InputDecoration.collapsed( |                 decoration: InputDecoration.collapsed( | ||||||
|                       hintText: AppLocalizations.of(context)!.postContentPlaceholder, |                   hintText: | ||||||
|  |                       AppLocalizations.of(context)!.postContentPlaceholder, | ||||||
|                 ), |                 ), | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
| @@ -191,8 +192,6 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | |||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -119,12 +119,11 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | |||||||
|           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), |           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|       child: Center( |  | ||||||
|         child: Container( |  | ||||||
|           constraints: const BoxConstraints(maxWidth: 640), |  | ||||||
|       child: Column( |       child: Column( | ||||||
|         children: [ |         children: [ | ||||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |           _isSubmitting | ||||||
|  |               ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |               : Container(), | ||||||
|           FutureBuilder( |           FutureBuilder( | ||||||
|             future: auth.getProfiles(), |             future: auth.getProfiles(), | ||||||
|             builder: (context, snapshot) { |             builder: (context, snapshot) { | ||||||
| @@ -156,9 +155,11 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | |||||||
|                 keyboardType: TextInputType.multiline, |                 keyboardType: TextInputType.multiline, | ||||||
|                 controller: _textController, |                 controller: _textController, | ||||||
|                 decoration: InputDecoration.collapsed( |                 decoration: InputDecoration.collapsed( | ||||||
|                       hintText: AppLocalizations.of(context)!.postContentPlaceholder, |                   hintText: | ||||||
|  |                       AppLocalizations.of(context)!.postContentPlaceholder, | ||||||
|                 ), |                 ), | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                 onTapOutside: (_) => | ||||||
|  |                     FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
| @@ -181,8 +182,6 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | |||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,10 +24,12 @@ class PostScreen extends StatefulWidget { | |||||||
| class _PostScreenState extends State<PostScreen> { | class _PostScreenState extends State<PostScreen> { | ||||||
|   final _client = http.Client(); |   final _client = http.Client(); | ||||||
|  |  | ||||||
|   final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0); |   final PagingController<int, Post> _commentPagingController = | ||||||
|  |       PagingController(firstPageKey: 0); | ||||||
|  |  | ||||||
|   Future<Post?> fetchPost(BuildContext context) async { |   Future<Post?> fetchPost(BuildContext context) async { | ||||||
|     final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}'); |     final uri = getRequestUri( | ||||||
|  |         'interactive', '/api/p/${widget.dataset}/${widget.alias}'); | ||||||
|     final res = await _client.get(uri); |     final res = await _client.get(uri); | ||||||
|     if (res.statusCode != 200) { |     if (res.statusCode != 200) { | ||||||
|       final err = utf8.decode(res.bodyBytes); |       final err = utf8.decode(res.bodyBytes); | ||||||
|   | |||||||
| @@ -1,108 +0,0 @@ | |||||||
| import 'package:flutter/material.dart'; |  | ||||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; |  | ||||||
| import 'package:provider/provider.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:url_launcher/url_launcher_string.dart'; |  | ||||||
|  |  | ||||||
| class SignInScreen extends StatelessWidget { |  | ||||||
|   final _usernameController = TextEditingController(); |  | ||||||
|   final _passwordController = TextEditingController(); |  | ||||||
|  |  | ||||||
|   SignInScreen({super.key}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final auth = context.read<AuthProvider>(); |  | ||||||
|  |  | ||||||
|     return IndentWrapper( |  | ||||||
|       title: AppLocalizations.of(context)!.signIn, |  | ||||||
|       hideDrawer: true, |  | ||||||
|       child: Center( |  | ||||||
|         child: Container( |  | ||||||
|           width: MediaQuery.of(context).size.width * 0.6, |  | ||||||
|           constraints: const BoxConstraints(maxWidth: 360), |  | ||||||
|           child: Column( |  | ||||||
|             mainAxisSize: MainAxisSize.min, |  | ||||||
|             children: [ |  | ||||||
|               TextField( |  | ||||||
|                 autocorrect: false, |  | ||||||
|                 enableSuggestions: false, |  | ||||||
|                 controller: _usernameController, |  | ||||||
|                 decoration: InputDecoration( |  | ||||||
|                   isDense: true, |  | ||||||
|                   border: const UnderlineInputBorder(), |  | ||||||
|                   labelText: AppLocalizations.of(context)!.username, |  | ||||||
|                 ), |  | ||||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|               ), |  | ||||||
|               const SizedBox(height: 12), |  | ||||||
|               TextField( |  | ||||||
|                 obscureText: true, |  | ||||||
|                 autocorrect: false, |  | ||||||
|                 enableSuggestions: false, |  | ||||||
|                 controller: _passwordController, |  | ||||||
|                 decoration: InputDecoration( |  | ||||||
|                   isDense: true, |  | ||||||
|                   border: const UnderlineInputBorder(), |  | ||||||
|                   labelText: AppLocalizations.of(context)!.password, |  | ||||||
|                 ), |  | ||||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|               ), |  | ||||||
|               const SizedBox(height: 16), |  | ||||||
|               TextButton( |  | ||||||
|                 child: Text(AppLocalizations.of(context)!.next), |  | ||||||
|                 onPressed: () { |  | ||||||
|                   final username = _usernameController.value.text; |  | ||||||
|                   final password = _passwordController.value.text; |  | ||||||
|                   if (username.isEmpty || password.isEmpty) return; |  | ||||||
|                   auth.signin(context, username, password).then((_) { |  | ||||||
|                     router.pop(true); |  | ||||||
|                   }).catchError((e) { |  | ||||||
|                     List<String> messages = e.toString().split('\n'); |  | ||||||
|                     if (messages.last.contains("risk")) { |  | ||||||
|                       final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last); |  | ||||||
|                       if (ticketId == null) { |  | ||||||
|                         context |  | ||||||
|                             .showErrorDialog("requested to multi-factor authenticate, but the ticket id was not found"); |  | ||||||
|                       } |  | ||||||
|                       showDialog( |  | ||||||
|                         context: context, |  | ||||||
|                         builder: (context) { |  | ||||||
|                           return AlertDialog( |  | ||||||
|                             title: Text(AppLocalizations.of(context)!.riskDetection), |  | ||||||
|                             content: Text(AppLocalizations.of(context)!.signInRiskDetected), |  | ||||||
|                             actions: [ |  | ||||||
|                               TextButton( |  | ||||||
|                                 child: Text(AppLocalizations.of(context)!.next), |  | ||||||
|                                 onPressed: () { |  | ||||||
|                                   launchUrlString( |  | ||||||
|                                     getRequestUri('passport', '/mfa?ticket=${ticketId!.group(1)}').toString(), |  | ||||||
|                                   ); |  | ||||||
|                                   if (Navigator.canPop(context)) { |  | ||||||
|                                     Navigator.pop(context); |  | ||||||
|                                   } |  | ||||||
|                                 }, |  | ||||||
|                               ) |  | ||||||
|                             ], |  | ||||||
|                           ); |  | ||||||
|                         }, |  | ||||||
|                       ); |  | ||||||
|                     } else { |  | ||||||
|                       ScaffoldMessenger.of(context).showSnackBar(SnackBar( |  | ||||||
|                         content: Text(messages.last), |  | ||||||
|                       )); |  | ||||||
|                     } |  | ||||||
|                   }); |  | ||||||
|                 }, |  | ||||||
|               ) |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -37,7 +37,8 @@ class _FriendPickerState extends State<FriendPicker> { | |||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: [ |       children: [ | ||||||
|         Container( |         Container( | ||||||
|           padding: const EdgeInsets.only(left: 16, right: 16, top: 32, bottom: 12), |           padding: | ||||||
|  |               const EdgeInsets.only(left: 16, right: 16, top: 32, bottom: 12), | ||||||
|           child: Text( |           child: Text( | ||||||
|             AppLocalizations.of(context)!.friend, |             AppLocalizations.of(context)!.friend, | ||||||
|             style: Theme.of(context).textTheme.headlineSmall, |             style: Theme.of(context).textTheme.headlineSmall, | ||||||
|   | |||||||
| @@ -41,7 +41,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     participant.addListener(onChange); |     participant.addListener(onChange); | ||||||
|     _subscription = Hardware.instance.onDeviceChange.stream.listen((List<MediaDevice> devices) { |     _subscription = Hardware.instance.onDeviceChange.stream | ||||||
|  |         .listen((List<MediaDevice> devices) { | ||||||
|       revertDevices(devices); |       revertDevices(devices); | ||||||
|     }); |     }); | ||||||
|     Hardware.instance.enumerateDevices().then(revertDevices); |     Hardware.instance.enumerateDevices().then(revertDevices); | ||||||
| @@ -161,18 +162,23 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|           if (!isRetry) { |           if (!isRetry) { | ||||||
|             const androidConfig = FlutterBackgroundAndroidConfig( |             const androidConfig = FlutterBackgroundAndroidConfig( | ||||||
|               notificationTitle: 'Screen Sharing', |               notificationTitle: 'Screen Sharing', | ||||||
|               notificationText: 'A Solar Messager\'s Call is sharing your screen', |               notificationText: | ||||||
|  |                   'A Solar Messager\'s Call is sharing your screen', | ||||||
|               notificationImportance: AndroidNotificationImportance.Default, |               notificationImportance: AndroidNotificationImportance.Default, | ||||||
|               notificationIcon: AndroidResource(name: 'launcher_icon', defType: 'mipmap'), |               notificationIcon: | ||||||
|  |                   AndroidResource(name: 'launcher_icon', defType: 'mipmap'), | ||||||
|             ); |             ); | ||||||
|             hasPermissions = await FlutterBackground.initialize(androidConfig: androidConfig); |             hasPermissions = await FlutterBackground.initialize( | ||||||
|  |                 androidConfig: androidConfig); | ||||||
|           } |           } | ||||||
|           if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) { |           if (hasPermissions && | ||||||
|  |               !FlutterBackground.isBackgroundExecutionEnabled) { | ||||||
|             await FlutterBackground.enableBackgroundExecution(); |             await FlutterBackground.enableBackgroundExecution(); | ||||||
|           } |           } | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|           if (!isRetry) { |           if (!isRetry) { | ||||||
|             return await Future<void>.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true)); |             return await Future<void>.delayed(const Duration(seconds: 1), | ||||||
|  |                 () => requestBackgroundPermission(true)); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -223,7 +229,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|         runSpacing: 5, |         runSpacing: 5, | ||||||
|         children: [ |         children: [ | ||||||
|           IconButton( |           IconButton( | ||||||
|             icon: Transform.flip(flipX: true, child: const Icon(Icons.exit_to_app)), |             icon: Transform.flip( | ||||||
|  |                 flipX: true, child: const Icon(Icons.exit_to_app)), | ||||||
|             color: Theme.of(context).colorScheme.onSurface, |             color: Theme.of(context).colorScheme.onSurface, | ||||||
|             onPressed: disconnect, |             onPressed: disconnect, | ||||||
|           ), |           ), | ||||||
| @@ -253,7 +260,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|                         return PopupMenuItem<MediaDevice>( |                         return PopupMenuItem<MediaDevice>( | ||||||
|                           value: device, |                           value: device, | ||||||
|                           child: ListTile( |                           child: ListTile( | ||||||
|                             leading: (device.deviceId == widget.room.selectedAudioInputDeviceId) |                             leading: (device.deviceId == | ||||||
|  |                                     widget.room.selectedAudioInputDeviceId) | ||||||
|                                 ? const Icon(Icons.check_box_outlined) |                                 ? const Icon(Icons.check_box_outlined) | ||||||
|                                 : const Icon(Icons.check_box_outline_blank), |                                 : const Icon(Icons.check_box_outline_blank), | ||||||
|                             title: Text(device.label), |                             title: Text(device.label), | ||||||
| @@ -281,7 +289,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|                     onTap: disableVideo, |                     onTap: disableVideo, | ||||||
|                     child: ListTile( |                     child: ListTile( | ||||||
|                       leading: const Icon(Icons.videocam_off), |                       leading: const Icon(Icons.videocam_off), | ||||||
|                       title: Text(AppLocalizations.of(context)!.chatCallVideoOff), |                       title: | ||||||
|  |                           Text(AppLocalizations.of(context)!.chatCallVideoOff), | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|                   if (_videoInputs != null) |                   if (_videoInputs != null) | ||||||
| @@ -289,7 +298,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|                       return PopupMenuItem<MediaDevice>( |                       return PopupMenuItem<MediaDevice>( | ||||||
|                         value: device, |                         value: device, | ||||||
|                         child: ListTile( |                         child: ListTile( | ||||||
|                           leading: (device.deviceId == widget.room.selectedVideoInputDeviceId) |                           leading: (device.deviceId == | ||||||
|  |                                   widget.room.selectedVideoInputDeviceId) | ||||||
|                               ? const Icon(Icons.check_box_outlined) |                               ? const Icon(Icons.check_box_outlined) | ||||||
|                               : const Icon(Icons.check_box_outline_blank), |                               : const Icon(Icons.check_box_outline_blank), | ||||||
|                           title: Text(device.label), |                           title: Text(device.label), | ||||||
| @@ -308,7 +318,9 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|               tooltip: AppLocalizations.of(context)!.chatCallVideoOn, |               tooltip: AppLocalizations.of(context)!.chatCallVideoOn, | ||||||
|             ), |             ), | ||||||
|           IconButton( |           IconButton( | ||||||
|             icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front), |             icon: Icon(position == CameraPosition.back | ||||||
|  |                 ? Icons.video_camera_back | ||||||
|  |                 : Icons.video_camera_front), | ||||||
|             color: Theme.of(context).colorScheme.onSurface, |             color: Theme.of(context).colorScheme.onSurface, | ||||||
|             onPressed: () => toggleCamera(), |             onPressed: () => toggleCamera(), | ||||||
|             tooltip: AppLocalizations.of(context)!.chatCallVideoFlip, |             tooltip: AppLocalizations.of(context)!.chatCallVideoFlip, | ||||||
| @@ -330,7 +342,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|                       return PopupMenuItem<MediaDevice>( |                       return PopupMenuItem<MediaDevice>( | ||||||
|                         value: device, |                         value: device, | ||||||
|                         child: ListTile( |                         child: ListTile( | ||||||
|                           leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId) |                           leading: (device.deviceId == | ||||||
|  |                                   widget.room.selectedAudioOutputDeviceId) | ||||||
|                               ? const Icon(Icons.check_box_outlined) |                               ? const Icon(Icons.check_box_outlined) | ||||||
|                               : const Icon(Icons.check_box_outline_blank), |                               : const Icon(Icons.check_box_outline_blank), | ||||||
|                           title: Text(device.label), |                           title: Text(device.label), | ||||||
| @@ -343,9 +356,12 @@ class _ControlsWidgetState extends State<ControlsWidget> { | |||||||
|             ), |             ), | ||||||
|           if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) |           if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) | ||||||
|             IconButton( |             IconButton( | ||||||
|               onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null, |               onPressed: Hardware.instance.canSwitchSpeakerphone | ||||||
|  |                   ? setSpeakerphoneOn | ||||||
|  |                   : null, | ||||||
|               color: Theme.of(context).colorScheme.onSurface, |               color: Theme.of(context).colorScheme.onSurface, | ||||||
|               icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android), |               icon: Icon( | ||||||
|  |                   _speakerphoneOn ? Icons.speaker_phone : Icons.phone_android), | ||||||
|               tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker, |               tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker, | ||||||
|             ), |             ), | ||||||
|           if (participant.isScreenShareEnabled()) |           if (participant.isScreenShareEnabled()) | ||||||
|   | |||||||
| @@ -20,7 +20,8 @@ class NoContentWidget extends StatefulWidget { | |||||||
|   State<NoContentWidget> createState() => _NoContentWidgetState(); |   State<NoContentWidget> createState() => _NoContentWidgetState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProviderStateMixin { | class _NoContentWidgetState extends State<NoContentWidget> | ||||||
|  |     with SingleTickerProviderStateMixin { | ||||||
|   late final AnimationController _animationController; |   late final AnimationController _animationController; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -35,7 +36,9 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv | |||||||
|     if (widget.isSpeaking) { |     if (widget.isSpeaking) { | ||||||
|       _animationController.repeat(reverse: true); |       _animationController.repeat(reverse: true); | ||||||
|     } else { |     } else { | ||||||
|       _animationController.animateTo(0, duration: 300.ms).then((_) => _animationController.reset()); |       _animationController | ||||||
|  |           .animateTo(0, duration: 300.ms) | ||||||
|  |           .then((_) => _animationController.reset()); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -63,7 +66,9 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv | |||||||
|               builder: (context, value, child) => Container( |               builder: (context, value, child) => Container( | ||||||
|                 decoration: BoxDecoration( |                 decoration: BoxDecoration( | ||||||
|                   borderRadius: BorderRadius.all(Radius.circular(radius + 8)), |                   borderRadius: BorderRadius.all(Radius.circular(radius + 8)), | ||||||
|                   border: value > 0 ? Border.all(color: Colors.green, width: value) : null, |                   border: value > 0 | ||||||
|  |                       ? Border.all(color: Colors.green, width: value) | ||||||
|  |                       : null, | ||||||
|                 ), |                 ), | ||||||
|                 child: child, |                 child: child, | ||||||
|               ), |               ), | ||||||
|   | |||||||
| @@ -95,7 +95,8 @@ class RemoteParticipantWidget extends ParticipantWidget { | |||||||
|   State<StatefulWidget> createState() => _RemoteParticipantWidgetState(); |   State<StatefulWidget> createState() => _RemoteParticipantWidgetState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> { | abstract class _ParticipantWidgetState<T extends ParticipantWidget> | ||||||
|  |     extends State<T> { | ||||||
|   VideoTrack? get _activeVideoTrack; |   VideoTrack? get _activeVideoTrack; | ||||||
|  |  | ||||||
|   TrackPublication? get _firstAudioPublication; |   TrackPublication? get _firstAudioPublication; | ||||||
| @@ -126,7 +127,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat | |||||||
|   void onParticipantChanged() { |   void onParticipantChanged() { | ||||||
|     setState(() { |     setState(() { | ||||||
|       if (widget.participant.metadata != null) { |       if (widget.participant.metadata != null) { | ||||||
|         _userinfoMetadata = Account.fromJson(jsonDecode(widget.participant.metadata!)); |         _userinfoMetadata = | ||||||
|  |             Account.fromJson(jsonDecode(widget.participant.metadata!)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -158,8 +160,11 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat | |||||||
|             mainAxisSize: MainAxisSize.min, |             mainAxisSize: MainAxisSize.min, | ||||||
|             children: [ |             children: [ | ||||||
|               ParticipantInfoWidget( |               ParticipantInfoWidget( | ||||||
|                 title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity, |                 title: widget.participant.name.isNotEmpty | ||||||
|                 audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true, |                     ? widget.participant.name | ||||||
|  |                     : widget.participant.identity, | ||||||
|  |                 audioAvailable: _firstAudioPublication?.muted == false && | ||||||
|  |                     _firstAudioPublication?.subscribed == true, | ||||||
|                 connectionQuality: widget.participant.connectionQuality, |                 connectionQuality: widget.participant.connectionQuality, | ||||||
|                 isScreenShare: widget.isScreenShare, |                 isScreenShare: widget.isScreenShare, | ||||||
|               ), |               ), | ||||||
| @@ -171,7 +176,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> { | class _LocalParticipantWidgetState | ||||||
|  |     extends _ParticipantWidgetState<LocalParticipantWidget> { | ||||||
|   @override |   @override | ||||||
|   LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication => |   LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication => | ||||||
|       widget.participant.audioTrackPublications.firstOrNull; |       widget.participant.audioTrackPublications.firstOrNull; | ||||||
| @@ -180,7 +186,8 @@ class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticip | |||||||
|   VideoTrack? get _activeVideoTrack => widget.videoTrack; |   VideoTrack? get _activeVideoTrack => widget.videoTrack; | ||||||
| } | } | ||||||
|  |  | ||||||
| class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> { | class _RemoteParticipantWidgetState | ||||||
|  |     extends _ParticipantWidgetState<RemoteParticipantWidget> { | ||||||
|   @override |   @override | ||||||
|   RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => |   RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => | ||||||
|       widget.participant.audioTrackPublications.firstOrNull; |       widget.participant.audioTrackPublications.firstOrNull; | ||||||
|   | |||||||
| @@ -55,7 +55,9 @@ class ParticipantInfoWidget extends StatelessWidget { | |||||||
|               Padding( |               Padding( | ||||||
|                 padding: const EdgeInsets.only(left: 5), |                 padding: const EdgeInsets.only(left: 5), | ||||||
|                 child: Icon( |                 child: Icon( | ||||||
|                   connectionQuality == ConnectionQuality.poor ? Icons.wifi_off_outlined : Icons.wifi, |                   connectionQuality == ConnectionQuality.poor | ||||||
|  |                       ? Icons.wifi_off_outlined | ||||||
|  |                       : Icons.wifi, | ||||||
|                   color: { |                   color: { | ||||||
|                     ConnectionQuality.excellent: Colors.green, |                     ConnectionQuality.excellent: Colors.green, | ||||||
|                     ConnectionQuality.good: Colors.orange, |                     ConnectionQuality.good: Colors.orange, | ||||||
|   | |||||||
| @@ -22,7 +22,9 @@ class ParticipantMenu extends StatefulWidget { | |||||||
|  |  | ||||||
| class _ParticipantMenuState extends State<ParticipantMenu> { | class _ParticipantMenuState extends State<ParticipantMenu> { | ||||||
|   RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication => |   RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication => | ||||||
|       widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull; |       widget.participant.videoTrackPublications | ||||||
|  |           .where((element) => element.sid == widget.videoTrack?.sid) | ||||||
|  |           .firstOrNull; | ||||||
|  |  | ||||||
|   RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => |   RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => | ||||||
|       widget.participant.audioTrackPublications.firstOrNull; |       widget.participant.audioTrackPublications.firstOrNull; | ||||||
| @@ -39,7 +41,8 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | |||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: [ |       children: [ | ||||||
|         Container( |         Container( | ||||||
|           padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), |           padding: | ||||||
|  |               const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), | ||||||
|           child: Padding( |           child: Padding( | ||||||
|             padding: const EdgeInsets.symmetric( |             padding: const EdgeInsets.symmetric( | ||||||
|               horizontal: 8, |               horizontal: 8, | ||||||
| @@ -59,9 +62,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | |||||||
|                   leading: Icon( |                   leading: Icon( | ||||||
|                     Icons.volume_up, |                     Icons.volume_up, | ||||||
|                     color: { |                     color: { | ||||||
|                       TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, |                       TrackSubscriptionState.notAllowed: | ||||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), |                           Theme.of(context).colorScheme.error, | ||||||
|                       TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, |                       TrackSubscriptionState.unsubscribed: Theme.of(context) | ||||||
|  |                           .colorScheme | ||||||
|  |                           .onSurface | ||||||
|  |                           .withOpacity(0.6), | ||||||
|  |                       TrackSubscriptionState.subscribed: | ||||||
|  |                           Theme.of(context).colorScheme.primary, | ||||||
|                     }[_firstAudioPublication!.subscriptionState], |                     }[_firstAudioPublication!.subscriptionState], | ||||||
|                   ), |                   ), | ||||||
|                   title: Text( |                   title: Text( | ||||||
| @@ -83,9 +91,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | |||||||
|                   leading: Icon( |                   leading: Icon( | ||||||
|                     widget.isScreenShare ? Icons.monitor : Icons.videocam, |                     widget.isScreenShare ? Icons.monitor : Icons.videocam, | ||||||
|                     color: { |                     color: { | ||||||
|                       TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, |                       TrackSubscriptionState.notAllowed: | ||||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), |                           Theme.of(context).colorScheme.error, | ||||||
|                       TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, |                       TrackSubscriptionState.unsubscribed: Theme.of(context) | ||||||
|  |                           .colorScheme | ||||||
|  |                           .onSurface | ||||||
|  |                           .withOpacity(0.6), | ||||||
|  |                       TrackSubscriptionState.subscribed: | ||||||
|  |                           Theme.of(context).colorScheme.primary, | ||||||
|                     }[_videoPublication!.subscriptionState], |                     }[_videoPublication!.subscriptionState], | ||||||
|                   ), |                   ), | ||||||
|                   title: Text( |                   title: Text( | ||||||
| @@ -107,7 +120,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | |||||||
|                 ...[30, 15, 8].map( |                 ...[30, 15, 8].map( | ||||||
|                   (x) => ListTile( |                   (x) => ListTile( | ||||||
|                     leading: Icon( |                     leading: Icon( | ||||||
|                       _videoPublication?.fps == x ? Icons.check_box_outlined : Icons.check_box_outline_blank, |                       _videoPublication?.fps == x | ||||||
|  |                           ? Icons.check_box_outlined | ||||||
|  |                           : Icons.check_box_outline_blank, | ||||||
|                     ), |                     ), | ||||||
|                     title: Text('Set preferred frame-per-second to $x'), |                     title: Text('Set preferred frame-per-second to $x'), | ||||||
|                     onTap: () { |                     onTap: () { | ||||||
| @@ -125,7 +140,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | |||||||
|                 ].map( |                 ].map( | ||||||
|                   (x) => ListTile( |                   (x) => ListTile( | ||||||
|                     leading: Icon( |                     leading: Icon( | ||||||
|                       _videoPublication?.videoQuality == x.$2 ? Icons.check_box_outlined : Icons.check_box_outline_blank, |                       _videoPublication?.videoQuality == x.$2 | ||||||
|  |                           ? Icons.check_box_outlined | ||||||
|  |                           : Icons.check_box_outline_blank, | ||||||
|                     ), |                     ), | ||||||
|                     title: Text('Set preferred quality to ${x.$1}'), |                     title: Text('Set preferred quality to ${x.$1}'), | ||||||
|                     onTap: () { |                     onTap: () { | ||||||
|   | |||||||
| @@ -28,11 +28,14 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | |||||||
|             stats['layer-$key'] = |             stats['layer-$key'] = | ||||||
|                 '${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps'; |                 '${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps'; | ||||||
|           }); |           }); | ||||||
|           var firstStats = event.stats['f'] ?? event.stats['h'] ?? event.stats['q']; |           var firstStats = | ||||||
|  |               event.stats['f'] ?? event.stats['h'] ?? event.stats['q']; | ||||||
|           if (firstStats != null) { |           if (firstStats != null) { | ||||||
|             stats['encoder'] = firstStats.encoderImplementation ?? ''; |             stats['encoder'] = firstStats.encoderImplementation ?? ''; | ||||||
|             stats['video codec'] = '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}'; |             stats['video codec'] = | ||||||
|             stats['qualityLimitationReason'] = firstStats.qualityLimitationReason ?? ''; |                 '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}'; | ||||||
|  |             stats['qualityLimitationReason'] = | ||||||
|  |                 firstStats.qualityLimitationReason ?? ''; | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| @@ -41,7 +44,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | |||||||
|       listener.on<VideoReceiverStatsEvent>((event) { |       listener.on<VideoReceiverStatsEvent>((event) { | ||||||
|         setState(() { |         setState(() { | ||||||
|           stats['video rx'] = '${event.currentBitrate.toInt()} kpbs'; |           stats['video rx'] = '${event.currentBitrate.toInt()} kpbs'; | ||||||
|           stats['video codec'] = '${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}'; |           stats['video codec'] = | ||||||
|  |               '${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}'; | ||||||
|           stats['video size'] = |           stats['video size'] = | ||||||
|               '${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps'; |               '${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps'; | ||||||
|           stats['video jitter'] = '${event.stats.jitter} s'; |           stats['video jitter'] = '${event.stats.jitter} s'; | ||||||
| @@ -70,7 +74,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | |||||||
|           stats['audio codec'] = |           stats['audio codec'] = | ||||||
|               '${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}'; |               '${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}'; | ||||||
|           stats['audio jitter'] = '${event.stats.jitter} s'; |           stats['audio jitter'] = '${event.stats.jitter} s'; | ||||||
|           stats['audio concealed samples'] = '${event.stats.concealedSamples} / ${event.stats.concealmentEvents}'; |           stats['audio concealed samples'] = | ||||||
|  |               '${event.stats.concealedSamples} / ${event.stats.concealmentEvents}'; | ||||||
|           stats['audio packets lost'] = '${event.stats.packetsLost}'; |           stats['audio packets lost'] = '${event.stats.packetsLost}'; | ||||||
|           stats['audio packets received'] = '${event.stats.packetsReceived}'; |           stats['audio packets received'] = '${event.stats.packetsReceived}'; | ||||||
|         }); |         }); | ||||||
| @@ -83,7 +88,10 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | |||||||
|       element.dispose(); |       element.dispose(); | ||||||
|     } |     } | ||||||
|     listeners.clear(); |     listeners.clear(); | ||||||
|     for (var track in [...widget.participant.videoTrackPublications, ...widget.participant.audioTrackPublications]) { |     for (var track in [ | ||||||
|  |       ...widget.participant.videoTrackPublications, | ||||||
|  |       ...widget.participant.audioTrackPublications | ||||||
|  |     ]) { | ||||||
|       if (track.track != null) { |       if (track.track != null) { | ||||||
|         _setUpListener(track.track!); |         _setUpListener(track.track!); | ||||||
|       } |       } | ||||||
| @@ -117,7 +125,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | |||||||
|         horizontal: 8, |         horizontal: 8, | ||||||
|       ), |       ), | ||||||
|       child: Column( |       child: Column( | ||||||
|         children: stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(), |         children: | ||||||
|  |             stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -14,7 +14,8 @@ class ChannelCallAction extends StatefulWidget { | |||||||
|   final Channel channel; |   final Channel channel; | ||||||
|   final Function onUpdate; |   final Function onUpdate; | ||||||
|  |  | ||||||
|   const ChannelCallAction({super.key, this.call, required this.channel, required this.onUpdate}); |   const ChannelCallAction( | ||||||
|  |       {super.key, this.call, required this.channel, required this.onUpdate}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ChannelCallAction> createState() => _ChannelCallActionState(); |   State<ChannelCallAction> createState() => _ChannelCallActionState(); | ||||||
| @@ -32,7 +33,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/calls'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.channel.alias}/calls'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.post(uri); |     var res = await auth.client!.post(uri); | ||||||
|     if (res.statusCode != 200) { |     if (res.statusCode != 200) { | ||||||
| @@ -52,7 +54,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.channel.alias}/calls/ongoing'); |     var uri = getRequestUri( | ||||||
|  |         'messaging', '/api/channels/${widget.channel.alias}/calls/ongoing'); | ||||||
|  |  | ||||||
|     var res = await auth.client!.delete(uri); |     var res = await auth.client!.delete(uri); | ||||||
|     if (res.statusCode != 200) { |     if (res.statusCode != 200) { | ||||||
| @@ -75,7 +78,9 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | |||||||
|                 endsCall(); |                 endsCall(); | ||||||
|               } |               } | ||||||
|             }, |             }, | ||||||
|       icon: widget.call == null ? const Icon(Icons.call) : const Icon(Icons.call_end), |       icon: widget.call == null | ||||||
|  |           ? const Icon(Icons.call) | ||||||
|  |           : const Icon(Icons.call_end), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -84,7 +89,8 @@ class ChannelManageAction extends StatelessWidget { | |||||||
|   final Channel channel; |   final Channel channel; | ||||||
|   final Function onUpdate; |   final Function onUpdate; | ||||||
|  |  | ||||||
|   const ChannelManageAction({super.key, required this.channel, required this.onUpdate}); |   const ChannelManageAction( | ||||||
|  |       {super.key, required this.channel, required this.onUpdate}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|   | |||||||
| @@ -12,7 +12,8 @@ class ChannelDeletion extends StatefulWidget { | |||||||
|   final Channel channel; |   final Channel channel; | ||||||
|   final bool isOwned; |   final bool isOwned; | ||||||
|  |  | ||||||
|   const ChannelDeletion({super.key, required this.channel, required this.isOwned}); |   const ChannelDeletion( | ||||||
|  |       {super.key, required this.channel, required this.isOwned}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ChannelDeletion> createState() => _ChannelDeletionState(); |   State<ChannelDeletion> createState() => _ChannelDeletionState(); | ||||||
|   | |||||||
| @@ -55,19 +55,23 @@ class _ChatMaintainerState extends State<ChatMaintainer> { | |||||||
|           switch (result.method) { |           switch (result.method) { | ||||||
|             case 'messages.new': |             case 'messages.new': | ||||||
|               final payload = Message.fromJson(result.payload!); |               final payload = Message.fromJson(result.payload!); | ||||||
|               if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload); |               if (payload.channelId == widget.channel.id) | ||||||
|  |                 widget.onInsertMessage(payload); | ||||||
|               break; |               break; | ||||||
|             case 'messages.update': |             case 'messages.update': | ||||||
|               final payload = Message.fromJson(result.payload!); |               final payload = Message.fromJson(result.payload!); | ||||||
|               if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload); |               if (payload.channelId == widget.channel.id) | ||||||
|  |                 widget.onUpdateMessage(payload); | ||||||
|               break; |               break; | ||||||
|             case 'messages.burnt': |             case 'messages.burnt': | ||||||
|               final payload = Message.fromJson(result.payload!); |               final payload = Message.fromJson(result.payload!); | ||||||
|               if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload); |               if (payload.channelId == widget.channel.id) | ||||||
|  |                 widget.onDeleteMessage(payload); | ||||||
|               break; |               break; | ||||||
|             case 'calls.new': |             case 'calls.new': | ||||||
|               final payload = Call.fromJson(result.payload!); |               final payload = Call.fromJson(result.payload!); | ||||||
|               if (payload.channelId == widget.channel.id) widget.onCallStarted(payload); |               if (payload.channelId == widget.channel.id) | ||||||
|  |                 widget.onCallStarted(payload); | ||||||
|               break; |               break; | ||||||
|             case 'calls.end': |             case 'calls.end': | ||||||
|               final payload = Call.fromJson(result.payload!); |               final payload = Call.fromJson(result.payload!); | ||||||
|   | |||||||
| @@ -79,7 +79,9 @@ class ChatMessageAction extends StatelessWidget { | |||||||
|  |  | ||||||
|                   return ListView( |                   return ListView( | ||||||
|                     children: [ |                     children: [ | ||||||
|                       ...(snapshot.data['id'] == item.sender.account.externalId ? authorizedItems : List.empty()), |                       ...(snapshot.data['id'] == item.sender.account.externalId | ||||||
|  |                           ? authorizedItems | ||||||
|  |                           : List.empty()), | ||||||
|                       ListTile( |                       ListTile( | ||||||
|                         leading: const Icon(Icons.reply), |                         leading: const Icon(Icons.reply), | ||||||
|                         title: Text(AppLocalizations.of(context)!.reply), |                         title: Text(AppLocalizations.of(context)!.reply), | ||||||
|   | |||||||
| @@ -19,7 +19,8 @@ class ChatMessageDeletionDialog extends StatefulWidget { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ChatMessageDeletionDialog> createState() => _ChatMessageDeletionDialogState(); |   State<ChatMessageDeletionDialog> createState() => | ||||||
|  |       _ChatMessageDeletionDialogState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { | class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { | ||||||
| @@ -29,7 +30,8 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { | |||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|     if (!await auth.isAuthorized()) return; |     if (!await auth.isAuthorized()) return; | ||||||
|  |  | ||||||
|     final uri = getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.item.id}'); |     final uri = getRequestUri('messaging', | ||||||
|  |         '/api/channels/${widget.channel}/messages/${widget.item.id}'); | ||||||
|  |  | ||||||
|     setState(() => _isSubmitting = true); |     setState(() => _isSubmitting = true); | ||||||
|     final res = await auth.client!.delete(uri); |     final res = await auth.client!.delete(uri); | ||||||
|   | |||||||
| @@ -18,7 +18,12 @@ class ChatMessageEditor extends StatefulWidget { | |||||||
|   final Message? replying; |   final Message? replying; | ||||||
|   final Function? onReset; |   final Function? onReset; | ||||||
|  |  | ||||||
|   const ChatMessageEditor({super.key, required this.channel, this.editing, this.replying, this.onReset}); |   const ChatMessageEditor( | ||||||
|  |       {super.key, | ||||||
|  |       required this.channel, | ||||||
|  |       this.editing, | ||||||
|  |       this.replying, | ||||||
|  |       this.onReset}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ChatMessageEditor> createState() => _ChatMessageEditorState(); |   State<ChatMessageEditor> createState() => _ChatMessageEditorState(); | ||||||
| @@ -51,7 +56,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | |||||||
|  |  | ||||||
|     final uri = widget.editing == null |     final uri = widget.editing == null | ||||||
|         ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') |         ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') | ||||||
|         : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); |         : getRequestUri('messaging', | ||||||
|  |             '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); | ||||||
|  |  | ||||||
|     final req = Request(widget.editing == null ? "POST" : "PUT", uri); |     final req = Request(widget.editing == null ? "POST" : "PUT", uri); | ||||||
|     req.headers['Content-Type'] = 'application/json'; |     req.headers['Content-Type'] = 'application/json'; | ||||||
| @@ -84,7 +90,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | |||||||
|       setState(() { |       setState(() { | ||||||
|         _prevEditingId = widget.editing!.id; |         _prevEditingId = widget.editing!.id; | ||||||
|         _textController.text = widget.editing!.content; |         _textController.text = widget.editing!.content; | ||||||
|         _attachments = widget.editing!.attachments ?? List.empty(growable: true); |         _attachments = | ||||||
|  |             widget.editing!.attachments ?? List.empty(growable: true); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -147,11 +154,15 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | |||||||
|             children: [ |             children: [ | ||||||
|               badge.Badge( |               badge.Badge( | ||||||
|                 showBadge: _attachments.isNotEmpty, |                 showBadge: _attachments.isNotEmpty, | ||||||
|                 badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)), |                 badgeContent: Text(_attachments.length.toString(), | ||||||
|  |                     style: const TextStyle(color: Colors.white)), | ||||||
|                 position: badge.BadgePosition.custom(top: -2, end: 8), |                 position: badge.BadgePosition.custom(top: -2, end: 8), | ||||||
|                 child: TextButton( |                 child: TextButton( | ||||||
|                   style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), |                   style: TextButton.styleFrom( | ||||||
|                   onPressed: !_isSubmitting ? () => viewAttachments(context) : null, |                       shape: const CircleBorder(), | ||||||
|  |                       padding: const EdgeInsets.all(4)), | ||||||
|  |                   onPressed: | ||||||
|  |                       !_isSubmitting ? () => viewAttachments(context) : null, | ||||||
|                   child: const Icon(Icons.attach_file), |                   child: const Icon(Icons.attach_file), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
| @@ -163,14 +174,18 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | |||||||
|                   autocorrect: true, |                   autocorrect: true, | ||||||
|                   keyboardType: TextInputType.text, |                   keyboardType: TextInputType.text, | ||||||
|                   decoration: InputDecoration.collapsed( |                   decoration: InputDecoration.collapsed( | ||||||
|                     hintText: AppLocalizations.of(context)!.chatMessagePlaceholder, |                     hintText: | ||||||
|  |                         AppLocalizations.of(context)!.chatMessagePlaceholder, | ||||||
|                   ), |                   ), | ||||||
|                   onSubmitted: (_) => sendMessage(context), |                   onSubmitted: (_) => sendMessage(context), | ||||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                   onTapOutside: (_) => | ||||||
|  |                       FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|               TextButton( |               TextButton( | ||||||
|                 style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), |                 style: TextButton.styleFrom( | ||||||
|  |                     shape: const CircleBorder(), | ||||||
|  |                     padding: const EdgeInsets.all(4)), | ||||||
|                 onPressed: !_isSubmitting ? () => sendMessage(context) : null, |                 onPressed: !_isSubmitting ? () => sendMessage(context) : null, | ||||||
|                 child: const Icon(Icons.send), |                 child: const Icon(Icons.send), | ||||||
|               ) |               ) | ||||||
|   | |||||||
| @@ -8,7 +8,8 @@ extension SolianCommonExtensions on BuildContext { | |||||||
|       if (message.trim().isEmpty) return ''; |       if (message.trim().isEmpty) return ''; | ||||||
|       return message |       return message | ||||||
|           .split(' ') |           .split(' ') | ||||||
|           .map((element) => "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") |           .map((element) => | ||||||
|  |               "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") | ||||||
|           .join(" "); |           .join(" "); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,10 +22,12 @@ class IndentWrapper extends LayoutWrapper { | |||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: hideDrawer ? IconButton( |         leading: hideDrawer | ||||||
|  |             ? IconButton( | ||||||
|                 icon: const Icon(Icons.arrow_back), |                 icon: const Icon(Icons.arrow_back), | ||||||
|                 onPressed: () => router.pop(), |                 onPressed: () => router.pop(), | ||||||
|         ) : null, |               ) | ||||||
|  |             : null, | ||||||
|         title: Text(title), |         title: Text(title), | ||||||
|         actions: appBarActions, |         actions: appBarActions, | ||||||
|       ), |       ), | ||||||
|   | |||||||
| @@ -49,7 +49,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> pickImageToUpload(BuildContext context, ImageSource source) async { |   Future<void> pickImageToUpload( | ||||||
|  |       BuildContext context, ImageSource source) async { | ||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|     if (!await auth.isAuthorized()) return; |     if (!await auth.isAuthorized()) return; | ||||||
|  |  | ||||||
| @@ -74,7 +75,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> pickVideoToUpload(BuildContext context, ImageSource source) async { |   Future<void> pickVideoToUpload( | ||||||
|  |       BuildContext context, ImageSource source) async { | ||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|     if (!await auth.isAuthorized()) return; |     if (!await auth.isAuthorized()) return; | ||||||
|  |  | ||||||
| @@ -102,7 +104,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|   Future<void> uploadAttachment(File file, String hashcode) async { |   Future<void> uploadAttachment(File file, String hashcode) async { | ||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|  |  | ||||||
|     final req = MultipartRequest('POST', getRequestUri(widget.provider, '/api/attachments')); |     final req = MultipartRequest( | ||||||
|  |         'POST', getRequestUri(widget.provider, '/api/attachments')); | ||||||
|     req.files.add(await MultipartFile.fromPath('attachment', file.path)); |     req.files.add(await MultipartFile.fromPath('attachment', file.path)); | ||||||
|     req.fields['hashcode'] = hashcode; |     req.fields['hashcode'] = hashcode; | ||||||
|  |  | ||||||
| @@ -118,10 +121,12 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> disposeAttachment(BuildContext context, Attachment item, int index) async { |   Future<void> disposeAttachment( | ||||||
|  |       BuildContext context, Attachment item, int index) async { | ||||||
|     final auth = context.read<AuthProvider>(); |     final auth = context.read<AuthProvider>(); | ||||||
|  |  | ||||||
|     final req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}')); |     final req = MultipartRequest('DELETE', | ||||||
|  |         getRequestUri(widget.provider, '/api/attachments/${item.id}')); | ||||||
|  |  | ||||||
|     setState(() => _isSubmitting = true); |     setState(() => _isSubmitting = true); | ||||||
|     var res = await auth.client!.send(req); |     var res = await auth.client!.send(req); | ||||||
| @@ -162,7 +167,17 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|     if (bytes == 0) return '0 Bytes'; |     if (bytes == 0) return '0 Bytes'; | ||||||
|     const k = 1024; |     const k = 1024; | ||||||
|     final dm = decimals < 0 ? 0 : decimals; |     final dm = decimals < 0 ? 0 : decimals; | ||||||
|     final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; |     final sizes = [ | ||||||
|  |       'Bytes', | ||||||
|  |       'KiB', | ||||||
|  |       'MiB', | ||||||
|  |       'GiB', | ||||||
|  |       'TiB', | ||||||
|  |       'PiB', | ||||||
|  |       'EiB', | ||||||
|  |       'ZiB', | ||||||
|  |       'YiB' | ||||||
|  |     ]; | ||||||
|     final i = (math.log(bytes) / math.log(k)).floor().toInt(); |     final i = (math.log(bytes) / math.log(k)).floor().toInt(); | ||||||
|     return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; |     return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; | ||||||
|   } |   } | ||||||
| @@ -180,7 +195,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|     return Column( |     return Column( | ||||||
|       children: [ |       children: [ | ||||||
|         Container( |         Container( | ||||||
|           padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), |           padding: | ||||||
|  |               const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), | ||||||
|           child: Row( |           child: Row( | ||||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, |             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|             children: [ |             children: [ | ||||||
| @@ -199,7 +215,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|                 builder: (context, snapshot) { |                 builder: (context, snapshot) { | ||||||
|                   if (snapshot.hasData && snapshot.data == true) { |                   if (snapshot.hasData && snapshot.data == true) { | ||||||
|                     return TextButton( |                     return TextButton( | ||||||
|                       onPressed: _isSubmitting ? null : () => viewAttachMethods(context), |                       onPressed: _isSubmitting | ||||||
|  |                           ? null | ||||||
|  |                           : () => viewAttachMethods(context), | ||||||
|                       style: TextButton.styleFrom(shape: const CircleBorder()), |                       style: TextButton.styleFrom(shape: const CircleBorder()), | ||||||
|                       child: const Icon(Icons.add_circle), |                       child: const Icon(Icons.add_circle), | ||||||
|                     ); |                     ); | ||||||
| @@ -211,7 +229,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|         _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |         _isSubmitting | ||||||
|  |             ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |             : Container(), | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: ListView.separated( |           child: ListView.separated( | ||||||
|             itemCount: _attachments.length, |             itemCount: _attachments.length, | ||||||
| @@ -243,7 +263,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | |||||||
|                         foregroundColor: Colors.red, |                         foregroundColor: Colors.red, | ||||||
|                       ), |                       ), | ||||||
|                       child: const Icon(Icons.delete), |                       child: const Icon(Icons.delete), | ||||||
|                       onPressed: () => disposeAttachment(context, element, index), |                       onPressed: () => | ||||||
|  |                           disposeAttachment(context, element, index), | ||||||
|                     ), |                     ), | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
| @@ -303,7 +324,8 @@ class AttachmentEditorMethodPopup extends StatelessWidget { | |||||||
|                     child: Column( |                     child: Column( | ||||||
|                       mainAxisSize: MainAxisSize.min, |                       mainAxisSize: MainAxisSize.min, | ||||||
|                       children: [ |                       children: [ | ||||||
|                         const Icon(Icons.add_photo_alternate, color: Colors.indigo), |                         const Icon(Icons.add_photo_alternate, | ||||||
|  |                             color: Colors.indigo), | ||||||
|                         const SizedBox(height: 8), |                         const SizedBox(height: 8), | ||||||
|                         Text(AppLocalizations.of(context)!.pickPhoto), |                         Text(AppLocalizations.of(context)!.pickPhoto), | ||||||
|                       ], |                       ], | ||||||
|   | |||||||
| @@ -36,6 +36,17 @@ class _AttachmentItemState extends State<AttachmentItem> { | |||||||
|   ); |   ); | ||||||
|   late final _videoController = VideoController(_videoPlayer); |   late final _videoController = VideoController(_videoPlayer); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     if (widget.type != 1) { | ||||||
|  |       _videoPlayer.open( | ||||||
|  |         Media(widget.url), | ||||||
|  |         play: false, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     const borderRadius = Radius.circular(8); |     const borderRadius = Radius.circular(8); | ||||||
| @@ -53,6 +64,7 @@ class _AttachmentItemState extends State<AttachmentItem> { | |||||||
|               children: [ |               children: [ | ||||||
|                 Image.network( |                 Image.network( | ||||||
|                   widget.url, |                   widget.url, | ||||||
|  |                   key: Key(getTag()), | ||||||
|                   width: double.infinity, |                   width: double.infinity, | ||||||
|                   height: double.infinity, |                   height: double.infinity, | ||||||
|                   fit: BoxFit.cover, |                   fit: BoxFit.cover, | ||||||
| @@ -63,6 +75,7 @@ class _AttachmentItemState extends State<AttachmentItem> { | |||||||
|                         right: 12, |                         right: 12, | ||||||
|                         bottom: 8, |                         bottom: 8, | ||||||
|                         child: Material( |                         child: Material( | ||||||
|  |                           color: Colors.transparent, | ||||||
|                           child: Chip(label: Text(widget.badge!)), |                           child: Chip(label: Text(widget.badge!)), | ||||||
|                         ), |                         ), | ||||||
|                       ) |                       ) | ||||||
| @@ -83,11 +96,6 @@ class _AttachmentItemState extends State<AttachmentItem> { | |||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       _videoPlayer.open( |  | ||||||
|         Media(widget.url), |  | ||||||
|         play: false, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       content = ClipRRect( |       content = ClipRRect( | ||||||
|         borderRadius: const BorderRadius.all(borderRadius), |         borderRadius: const BorderRadius.all(borderRadius), | ||||||
|         child: Video( |         child: Video( | ||||||
| @@ -121,9 +129,11 @@ class AttachmentList extends StatelessWidget { | |||||||
|   final List<Attachment> items; |   final List<Attachment> items; | ||||||
|   final String provider; |   final String provider; | ||||||
|  |  | ||||||
|   const AttachmentList({super.key, required this.items, required this.provider}); |   const AttachmentList( | ||||||
|  |       {super.key, required this.items, required this.provider}); | ||||||
|  |  | ||||||
|   Uri getFileUri(String fileId) => getRequestUri(provider, '/api/attachments/o/$fileId'); |   Uri getFileUri(String fileId) => | ||||||
|  |       getRequestUri(provider, '/api/attachments/o/$fileId'); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|   | |||||||
| @@ -47,7 +47,8 @@ class _PostItemState extends State<PostItem> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   void viewComments() { |   void viewComments() { | ||||||
|     final PagingController<int, Post> commentPaging = PagingController(firstPageKey: 0); |     final PagingController<int, Post> commentPaging = | ||||||
|  |         PagingController(firstPageKey: 0); | ||||||
|  |  | ||||||
|     showModalBottomSheet( |     showModalBottomSheet( | ||||||
|       context: context, |       context: context, | ||||||
| @@ -87,10 +88,12 @@ class _PostItemState extends State<PostItem> { | |||||||
|   Widget renderAttachments() { |   Widget renderAttachments() { | ||||||
|     if (widget.item.modelType == 'article') return Container(); |     if (widget.item.modelType == 'article') return Container(); | ||||||
|  |  | ||||||
|     if (widget.item.attachments != null && widget.item.attachments!.isNotEmpty) { |     if (widget.item.attachments != null && | ||||||
|  |         widget.item.attachments!.isNotEmpty) { | ||||||
|       return Padding( |       return Padding( | ||||||
|         padding: const EdgeInsets.only(top: 8), |         padding: const EdgeInsets.only(top: 8), | ||||||
|         child: AttachmentList(items: widget.item.attachments!, provider: 'interactive'), |         child: AttachmentList( | ||||||
|  |             items: widget.item.attachments!, provider: 'interactive'), | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       return Container(); |       return Container(); | ||||||
| @@ -130,8 +133,9 @@ class _PostItemState extends State<PostItem> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   String getAuthorDescribe() => |   String getAuthorDescribe() => widget.item.author.description.isNotEmpty | ||||||
|       widget.item.author.description.isNotEmpty ? widget.item.author.description : 'No description yet.'; |       ? widget.item.author.description | ||||||
|  |       : 'No description yet.'; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
| @@ -177,7 +181,8 @@ class _PostItemState extends State<PostItem> { | |||||||
|                     children: [ |                     children: [ | ||||||
|                       ...headingParts, |                       ...headingParts, | ||||||
|                       Padding( |                       Padding( | ||||||
|                         padding: const EdgeInsets.only(left: 12, right: 12, top: 4), |                         padding: | ||||||
|  |                             const EdgeInsets.only(left: 12, right: 12, top: 4), | ||||||
|                         child: renderContent(), |                         child: renderContent(), | ||||||
|                       ), |                       ), | ||||||
|                       renderAttachments(), |                       renderAttachments(), | ||||||
|   | |||||||
| @@ -106,7 +106,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> { | |||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: [ |       children: [ | ||||||
|         Container( |         Container( | ||||||
|           padding: const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), |           padding: | ||||||
|  |               const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12), | ||||||
|           child: Padding( |           child: Padding( | ||||||
|             padding: const EdgeInsets.symmetric( |             padding: const EdgeInsets.symmetric( | ||||||
|               horizontal: 8, |               horizontal: 8, | ||||||
| @@ -118,7 +119,9 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> { | |||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|         _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), |         _isSubmitting | ||||||
|  |             ? const LinearProgressIndicator().animate().scaleX() | ||||||
|  |             : Container(), | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: ListView.builder( |           child: ListView.builder( | ||||||
|             itemCount: reactions.length, |             itemCount: reactions.length, | ||||||
|   | |||||||
| @@ -36,5 +36,4 @@ class SignInRequiredScreen extends StatelessWidget { | |||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user