🎨 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 | ||||
|   # producing the lint. | ||||
|   rules: | ||||
|     # avoid_print: false  # Uncomment to disable the `avoid_print` rule | ||||
|     # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule | ||||
|     lines_longer_than_80_chars: false | ||||
|     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 | ||||
| # https://dart.dev/guides/language/analysis-options | ||||
|   | ||||
| @@ -16,6 +16,8 @@ | ||||
|   "confirmation": "Confirmation", | ||||
|   "confirmCancel": "Not sure", | ||||
|   "confirmOkay": "OK", | ||||
|   "email": "Email Address", | ||||
|   "nickname": "Nickname", | ||||
|   "username": "Username", | ||||
|   "password": "Password", | ||||
|   "next": "Next", | ||||
|   | ||||
| @@ -16,6 +16,8 @@ | ||||
|   "confirmation": "确认", | ||||
|   "confirmCancel": "不太确定", | ||||
|   "confirmOkay": "确定", | ||||
|   "email": "邮箱地址", | ||||
|   "nickname": "显示名", | ||||
|   "username": "用户名", | ||||
|   "password": "密码", | ||||
|   "next": "下一步", | ||||
|   | ||||
| @@ -25,28 +25,29 @@ class Call { | ||||
|   }); | ||||
|  | ||||
|   factory Call.fromJson(Map<String, dynamic> json) => Call( | ||||
|     id: json["id"], | ||||
|     createdAt: DateTime.parse(json["created_at"]), | ||||
|     updatedAt: DateTime.parse(json["updated_at"]), | ||||
|     deletedAt: json["deleted_at"], | ||||
|     endedAt: json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, | ||||
|     externalId: json["external_id"], | ||||
|     founderId: json["founder_id"], | ||||
|     channelId: json["channel_id"], | ||||
|     channel: Channel.fromJson(json["channel"]), | ||||
|   ); | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         endedAt: | ||||
|             json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, | ||||
|         externalId: json["external_id"], | ||||
|         founderId: json["founder_id"], | ||||
|         channelId: json["channel_id"], | ||||
|         channel: Channel.fromJson(json["channel"]), | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "id": id, | ||||
|     "created_at": createdAt.toIso8601String(), | ||||
|     "updated_at": updatedAt.toIso8601String(), | ||||
|     "deleted_at": deletedAt, | ||||
|     "ended_at": endedAt?.toIso8601String(), | ||||
|     "external_id": externalId, | ||||
|     "founder_id": founderId, | ||||
|     "channel_id": channelId, | ||||
|     "channel": channel.toJson(), | ||||
|   }; | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "ended_at": endedAt?.toIso8601String(), | ||||
|         "external_id": externalId, | ||||
|         "founder_id": founderId, | ||||
|         "channel_id": channelId, | ||||
|         "channel": channel.toJson(), | ||||
|       }; | ||||
| } | ||||
|  | ||||
| enum ParticipantStatsType { | ||||
| @@ -60,8 +61,8 @@ enum ParticipantStatsType { | ||||
| class ParticipantTrack { | ||||
|   ParticipantTrack( | ||||
|       {required this.participant, | ||||
|         required this.videoTrack, | ||||
|         required this.isScreenShare}); | ||||
|       required this.videoTrack, | ||||
|       required this.isScreenShare}); | ||||
|   VideoTrack? videoTrack; | ||||
|   Participant participant; | ||||
|   bool isScreenShare; | ||||
|   | ||||
| @@ -32,36 +32,36 @@ class Channel { | ||||
|   }); | ||||
|  | ||||
|   factory Channel.fromJson(Map<String, dynamic> json) => Channel( | ||||
|     id: json["id"], | ||||
|     createdAt: DateTime.parse(json["created_at"]), | ||||
|     updatedAt: DateTime.parse(json["updated_at"]), | ||||
|     deletedAt: json["deleted_at"], | ||||
|     alias: json["alias"], | ||||
|     name: json["name"], | ||||
|     description: json["description"], | ||||
|     members: json["members"], | ||||
|     calls: json["calls"], | ||||
|     type: json["type"], | ||||
|     account: Account.fromJson(json["account"]), | ||||
|     accountId: json["account_id"], | ||||
|     realmId: json["realm_id"], | ||||
|   ); | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         alias: json["alias"], | ||||
|         name: json["name"], | ||||
|         description: json["description"], | ||||
|         members: json["members"], | ||||
|         calls: json["calls"], | ||||
|         type: json["type"], | ||||
|         account: Account.fromJson(json["account"]), | ||||
|         accountId: json["account_id"], | ||||
|         realmId: json["realm_id"], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "id": id, | ||||
|     "created_at": createdAt.toIso8601String(), | ||||
|     "updated_at": updatedAt.toIso8601String(), | ||||
|     "deleted_at": deletedAt, | ||||
|     "alias": alias, | ||||
|     "name": name, | ||||
|     "description": description, | ||||
|     "members": members, | ||||
|     "calls": calls, | ||||
|     "type": type, | ||||
|     "account": account, | ||||
|     "account_id": accountId, | ||||
|     "realm_id": realmId, | ||||
|   }; | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "alias": alias, | ||||
|         "name": name, | ||||
|         "description": description, | ||||
|         "members": members, | ||||
|         "calls": calls, | ||||
|         "type": type, | ||||
|         "account": account, | ||||
|         "account_id": accountId, | ||||
|         "realm_id": realmId, | ||||
|       }; | ||||
| } | ||||
|  | ||||
| class ChannelMember { | ||||
| @@ -86,24 +86,24 @@ class ChannelMember { | ||||
|   }); | ||||
|  | ||||
|   factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember( | ||||
|     id: json["id"], | ||||
|     createdAt: DateTime.parse(json["created_at"]), | ||||
|     updatedAt: DateTime.parse(json["updated_at"]), | ||||
|     deletedAt: json["deleted_at"], | ||||
|     channelId: json["channel_id"], | ||||
|     accountId: json["account_id"], | ||||
|     account: Account.fromJson(json["account"]), | ||||
|     notify: json["notify"], | ||||
|   ); | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         channelId: json["channel_id"], | ||||
|         accountId: json["account_id"], | ||||
|         account: Account.fromJson(json["account"]), | ||||
|         notify: json["notify"], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "id": id, | ||||
|     "created_at": createdAt.toIso8601String(), | ||||
|     "updated_at": updatedAt.toIso8601String(), | ||||
|     "deleted_at": deletedAt, | ||||
|     "channel_id": channelId, | ||||
|     "account_id": accountId, | ||||
|     "account": account.toJson(), | ||||
|     "notify": notify, | ||||
|   }; | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "channel_id": channelId, | ||||
|         "account_id": accountId, | ||||
|         "account": account.toJson(), | ||||
|         "notify": notify, | ||||
|       }; | ||||
| } | ||||
| @@ -26,30 +26,30 @@ class Friendship { | ||||
|   }); | ||||
|  | ||||
|   factory Friendship.fromJson(Map<String, dynamic> json) => Friendship( | ||||
|     id: json["id"], | ||||
|     createdAt: DateTime.parse(json["created_at"]), | ||||
|     updatedAt: DateTime.parse(json["updated_at"]), | ||||
|     deletedAt: json["deleted_at"], | ||||
|     accountId: json["account_id"], | ||||
|     relatedId: json["related_id"], | ||||
|     blockedBy: json["blocked_by"], | ||||
|     account: Account.fromJson(json["account"]), | ||||
|     related: Account.fromJson(json["related"]), | ||||
|     status: json["status"], | ||||
|   ); | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         accountId: json["account_id"], | ||||
|         relatedId: json["related_id"], | ||||
|         blockedBy: json["blocked_by"], | ||||
|         account: Account.fromJson(json["account"]), | ||||
|         related: Account.fromJson(json["related"]), | ||||
|         status: json["status"], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "id": id, | ||||
|     "created_at": createdAt.toIso8601String(), | ||||
|     "updated_at": updatedAt.toIso8601String(), | ||||
|     "deleted_at": deletedAt, | ||||
|     "account_id": accountId, | ||||
|     "related_id": relatedId, | ||||
|     "blocked_by": blockedBy, | ||||
|     "account": account.toJson(), | ||||
|     "related": related.toJson(), | ||||
|     "status": status, | ||||
|   }; | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "account_id": accountId, | ||||
|         "related_id": relatedId, | ||||
|         "blocked_by": blockedBy, | ||||
|         "account": account.toJson(), | ||||
|         "related": related.toJson(), | ||||
|         "status": status, | ||||
|       }; | ||||
|  | ||||
|   Account getOtherside(int selfId) { | ||||
|     if (accountId != selfId) { | ||||
|   | ||||
| @@ -43,11 +43,15 @@ class Message { | ||||
|         content: json["content"], | ||||
|         metadata: json["metadata"], | ||||
|         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"]), | ||||
|         sender: Sender.fromJson(json["sender"]), | ||||
|         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"], | ||||
|         senderId: json["sender_id"], | ||||
|       ); | ||||
| @@ -60,7 +64,8 @@ class Message { | ||||
|         "content": content, | ||||
|         "metadata": metadata, | ||||
|         "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(), | ||||
|         "sender": sender.toJson(), | ||||
|         "reply_id": replyId, | ||||
|   | ||||
| @@ -34,7 +34,9 @@ class Notification { | ||||
|         deletedAt: json["deleted_at"], | ||||
|         subject: json["subject"], | ||||
|         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"], | ||||
|         isRealtime: json["is_realtime"], | ||||
|         readAt: json["read_at"], | ||||
| @@ -49,7 +51,9 @@ class Notification { | ||||
|         "deleted_at": deletedAt, | ||||
|         "subject": subject, | ||||
|         "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_realtime": isRealtime, | ||||
|         "read_at": readAt, | ||||
|   | ||||
| @@ -10,14 +10,14 @@ class NetworkPackage { | ||||
|   }); | ||||
|  | ||||
|   factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage( | ||||
|     method: json["w"], | ||||
|     message: json["m"], | ||||
|     payload: json["p"], | ||||
|   ); | ||||
|         method: json["w"], | ||||
|         message: json["m"], | ||||
|         payload: json["p"], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     "w": method, | ||||
|     "m": message, | ||||
|     "p": payload, | ||||
|   }; | ||||
|         "w": method, | ||||
|         "m": message, | ||||
|         "p": payload, | ||||
|       }; | ||||
| } | ||||
| @@ -8,7 +8,8 @@ import 'package:solian/utils/service_url.dart'; | ||||
| class AuthProvider extends ChangeNotifier { | ||||
|   AuthProvider(); | ||||
|  | ||||
|   final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe'); | ||||
|   final deviceEndpoint = | ||||
|       getRequestUri('passport', '/api/notifications/subscribe'); | ||||
|   final tokenEndpoint = getRequestUri('passport', '/api/auth/token'); | ||||
|   final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); | ||||
|   final redirectUrl = Uri.parse('solian://auth'); | ||||
| @@ -29,8 +30,10 @@ class AuthProvider extends ChangeNotifier { | ||||
|   Future<bool> loadClient() async { | ||||
|     if (await storage.containsKey(key: storageKey)) { | ||||
|       try { | ||||
|         final credentials = oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); | ||||
|         client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); | ||||
|         final credentials = | ||||
|             oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); | ||||
|         client = oauth2.Client(credentials, | ||||
|             identifier: clientId, secret: clientSecret); | ||||
|         await fetchProfiles(); | ||||
|         return true; | ||||
|       } 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()) { | ||||
|       return client!; | ||||
|     } | ||||
| @@ -68,15 +72,17 @@ class AuthProvider extends ChangeNotifier { | ||||
|  | ||||
|   Future<void> refreshToken() async { | ||||
|     if (client != null) { | ||||
|       final credentials = | ||||
|           await client!.credentials.refresh(identifier: clientId, secret: clientSecret, basicAuth: false); | ||||
|       client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); | ||||
|       final credentials = await client!.credentials.refresh( | ||||
|           identifier: clientId, secret: clientSecret, basicAuth: false); | ||||
|       client = oauth2.Client(credentials, | ||||
|           identifier: clientId, secret: clientSecret); | ||||
|       storage.write(key: storageKey, value: credentials.toJson()); | ||||
|     } | ||||
|     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); | ||||
|     storage.write(key: storageKey, value: client!.credentials.toJson()); | ||||
|  | ||||
| @@ -94,7 +100,10 @@ class AuthProvider extends ChangeNotifier { | ||||
|       if (client == null) { | ||||
|         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(); | ||||
|         lastRefreshedAt = DateTime.now(); | ||||
|       } | ||||
|   | ||||
| @@ -32,7 +32,9 @@ class ChatProvider extends ChangeNotifier { | ||||
|       scheme: ori.scheme.replaceFirst('http', 'ws'), | ||||
|       host: ori.host, | ||||
|       path: ori.path, | ||||
|       queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, | ||||
|       queryParameters: { | ||||
|         'tk': Uri.encodeComponent(auth.client!.credentials.accessToken) | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     final channel = WebSocketChannel.connect(uri); | ||||
| @@ -41,7 +43,8 @@ class ChatProvider extends ChangeNotifier { | ||||
|     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; | ||||
|  | ||||
|     this.call = ChatCallInstance( | ||||
| @@ -106,7 +109,8 @@ class ChatCallInstance { | ||||
|   }); | ||||
|  | ||||
|   void init() { | ||||
|     subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices); | ||||
|     subscription = | ||||
|         Hardware.instance.onDeviceChange.stream.listen(revertDevices); | ||||
|     room = Room(); | ||||
|     listener = room.createListener(); | ||||
|     Hardware.instance.enumerateDevices().then(revertDevices); | ||||
| @@ -114,7 +118,8 @@ class ChatCallInstance { | ||||
|   } | ||||
|  | ||||
|   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.microphone.request(); | ||||
| @@ -131,7 +136,8 @@ class ChatCallInstance { | ||||
|       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); | ||||
|     if (res.statusCode == 200) { | ||||
| @@ -184,10 +190,12 @@ class ChatCallInstance { | ||||
|             useiOSBroadcastExtension: true, | ||||
|             params: VideoParameters( | ||||
|               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( | ||||
|           microphone: TrackOption(track: audioTrack), | ||||
| @@ -220,7 +228,8 @@ class ChatCallInstance { | ||||
|     room.addListener(onRoomDidUpdate); | ||||
|     setupRoomListeners(context); | ||||
|     sortParticipants(); | ||||
|     WidgetsBindingCompatible.instance?.addPostFrameCallback((_) => autoPublish(context)); | ||||
|     WidgetsBindingCompatible.instance | ||||
|         ?.addPostFrameCallback((_) => autoPublish(context)); | ||||
|  | ||||
|     if (lkPlatformIsMobile()) { | ||||
|       Hardware.instance.setSpeakerphoneOn(true); | ||||
| @@ -295,7 +304,8 @@ class ChatCallInstance { | ||||
|       } | ||||
|  | ||||
|       // 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( | ||||
| @@ -304,7 +314,8 @@ class ChatCallInstance { | ||||
|       isScreenShare: false, | ||||
|     ); | ||||
|     if (room.localParticipant != null) { | ||||
|       final localParticipantTracks = room.localParticipant?.videoTrackPublications; | ||||
|       final localParticipantTracks = | ||||
|           room.localParticipant?.videoTrackPublications; | ||||
|       if (localParticipantTracks != null) { | ||||
|         for (var t in localParticipantTracks) { | ||||
|           localTrack.videoTrack = t.track; | ||||
| @@ -317,7 +328,8 @@ class ChatCallInstance { | ||||
|     if (focusTrack == null) { | ||||
|       focusTrack = participantTracks.first; | ||||
|     } 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]; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -16,8 +16,8 @@ class FriendProvider extends ChangeNotifier { | ||||
|     var res = await auth.client!.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>; | ||||
|      friends = result.map((x) => Friendship.fromJson(x)).toList(); | ||||
|      notifyListeners(); | ||||
|       friends = result.map((x) => Friendship.fromJson(x)).toList(); | ||||
|       notifyListeners(); | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       throw Exception(message); | ||||
|   | ||||
| @@ -17,7 +17,8 @@ class NotifyProvider extends ChangeNotifier { | ||||
|  | ||||
|   List<model.Notification> notifications = List.empty(growable: true); | ||||
|  | ||||
|   final FlutterLocalNotificationsPlugin localNotify = FlutterLocalNotificationsPlugin(); | ||||
|   final FlutterLocalNotificationsPlugin localNotify = | ||||
|       FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
|   NotifyProvider() { | ||||
|     initNotify(); | ||||
| @@ -31,8 +32,10 @@ class NotifyProvider extends ChangeNotifier { | ||||
|         DarwinNotificationCategory("general"), | ||||
|       ], | ||||
|     ); | ||||
|     const linuxSettings = LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|     const InitializationSettings initializationSettings = InitializationSettings( | ||||
|     const linuxSettings = | ||||
|         LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|     const InitializationSettings initializationSettings = | ||||
|         InitializationSettings( | ||||
|       android: androidSettings, | ||||
|       iOS: darwinSettings, | ||||
|       macOS: darwinSettings, | ||||
| @@ -43,7 +46,8 @@ class NotifyProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   Future<void> requestPermissions() async { | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) return; | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) | ||||
|       return; | ||||
|     await Permission.notification.request(); | ||||
|   } | ||||
|  | ||||
| @@ -53,8 +57,11 @@ class NotifyProvider extends ChangeNotifier { | ||||
|     var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25'); | ||||
|     var res = await auth.client!.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|       notifications = result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? List.empty(growable: true); | ||||
|       final result = | ||||
|           PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|       notifications = | ||||
|           result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? | ||||
|               List.empty(growable: true); | ||||
|     } | ||||
|  | ||||
|     notifyListeners(); | ||||
| @@ -71,7 +78,9 @@ class NotifyProvider extends ChangeNotifier { | ||||
|       scheme: ori.scheme.replaceFirst('http', 'ws'), | ||||
|       host: ori.host, | ||||
|       path: ori.path, | ||||
|       queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, | ||||
|       queryParameters: { | ||||
|         'tk': Uri.encodeComponent(auth.client!.credentials.accessToken) | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     final channel = WebSocketChannel.connect(uri); | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/screens/account.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/chat.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/moment_editor.dart'; | ||||
| import 'package:solian/screens/posts/screen.dart'; | ||||
| import 'package:solian/screens/signin.dart'; | ||||
| import 'package:solian/screens/auth/signin.dart'; | ||||
|  | ||||
| final router = GoRouter( | ||||
|   routes: [ | ||||
| @@ -37,12 +38,14 @@ final router = GoRouter( | ||||
|     GoRoute( | ||||
|       path: '/chat/create', | ||||
|       name: 'chat.channel.editor', | ||||
|       builder: (context, state) => ChannelEditorScreen(editing: state.extra as Channel?), | ||||
|       builder: (context, state) => | ||||
|           ChannelEditorScreen(editing: state.extra as Channel?), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/chat/c/: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( | ||||
|       path: '/chat/c/:channel/call', | ||||
| @@ -52,12 +55,14 @@ final router = GoRouter( | ||||
|     GoRoute( | ||||
|       path: '/chat/c/: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( | ||||
|       path: '/chat/c/: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( | ||||
|       path: '/account', | ||||
| @@ -67,14 +72,16 @@ final router = GoRouter( | ||||
|     GoRoute( | ||||
|       path: '/posts/publish/moments', | ||||
|       name: 'posts.moments.editor', | ||||
|       builder: (context, state) => MomentEditorScreen(editing: state.extra as Post?), | ||||
|       builder: (context, state) => | ||||
|           MomentEditorScreen(editing: state.extra as Post?), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/posts/publish/comments', | ||||
|       name: 'posts.comments.editor', | ||||
|       builder: (context, state) { | ||||
|         final args = state.extra as CommentPostArguments; | ||||
|         return CommentEditorScreen(editing: args.editing, related: args.related); | ||||
|         return CommentEditorScreen( | ||||
|             editing: args.editing, related: args.related); | ||||
|       }, | ||||
|     ), | ||||
|     GoRoute( | ||||
| @@ -90,6 +97,11 @@ final router = GoRouter( | ||||
|       name: 'auth.sign-in', | ||||
|       builder: (context, state) => SignInScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/auth/sign-up', | ||||
|       name: 'auth.sign-up', | ||||
|       builder: (context, state) => SignUpScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/account/friend', | ||||
|       name: 'account.friend', | ||||
|   | ||||
| @@ -83,7 +83,7 @@ class _AccountScreenState extends State<AccountScreen> { | ||||
|                     title: AppLocalizations.of(context)!.signUp, | ||||
|                     caption: AppLocalizations.of(context)!.signUpCaption, | ||||
|                     onTap: () { | ||||
|                       launchUrl(getRequestUri('passport', '/sign-up')); | ||||
|                       router.pushNamed('auth.sign-up'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
| @@ -131,7 +131,8 @@ class NameCard extends StatelessWidget { | ||||
|             children: [ | ||||
|               FutureBuilder( | ||||
|                 future: renderAvatar(context), | ||||
|                 builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { | ||||
|                 builder: | ||||
|                     (BuildContext context, AsyncSnapshot<Widget> snapshot) { | ||||
|                   if (snapshot.hasData) { | ||||
|                     return snapshot.data!; | ||||
|                   } else { | ||||
| @@ -142,7 +143,8 @@ class NameCard extends StatelessWidget { | ||||
|               const SizedBox(width: 20), | ||||
|               FutureBuilder( | ||||
|                 future: renderLabel(context), | ||||
|                 builder: (BuildContext context, AsyncSnapshot<Column> snapshot) { | ||||
|                 builder: | ||||
|                     (BuildContext context, AsyncSnapshot<Column> snapshot) { | ||||
|                   if (snapshot.hasData) { | ||||
|                     return snapshot.data!; | ||||
|                   } else { | ||||
| @@ -164,7 +166,12 @@ class ActionCard extends StatelessWidget { | ||||
|   final String caption; | ||||
|   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 | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -120,14 +120,16 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   labelText: AppLocalizations.of(context)!.username, | ||||
|                 ), | ||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 onTapOutside: (_) => | ||||
|                     FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           actions: <Widget>[ | ||||
|             TextButton( | ||||
|               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), | ||||
|               child: Text(AppLocalizations.of(context)!.cancel), | ||||
| @@ -155,7 +157,8 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|   DismissDirection getDismissDirection(Friendship relation) { | ||||
|     if (relation.status == 2) return DismissDirection.endToStart; | ||||
|     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; | ||||
|   } | ||||
|  | ||||
| @@ -220,12 +223,18 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|             SliverToBoxAdapter( | ||||
|               child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|               child: _isSubmitting | ||||
|                   ? const LinearProgressIndicator().animate().scaleX() | ||||
|                   : Container(), | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context) | ||||
|                     .colorScheme | ||||
|                     .surfaceVariant | ||||
|                     .withOpacity(0.8), | ||||
|                 child: Text(AppLocalizations.of(context)!.friendPending), | ||||
|               ), | ||||
|             ), | ||||
| @@ -235,8 +244,12 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context) | ||||
|                     .colorScheme | ||||
|                     .surfaceVariant | ||||
|                     .withOpacity(0.8), | ||||
|                 child: Text(AppLocalizations.of(context)!.friendActive), | ||||
|               ), | ||||
|             ), | ||||
| @@ -246,8 +259,12 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(horizontal: 18, vertical: 12), | ||||
|                 color: Theme.of(context) | ||||
|                     .colorScheme | ||||
|                     .surfaceVariant | ||||
|                     .withOpacity(0.8), | ||||
|                 child: Text(AppLocalizations.of(context)!.friendBlocked), | ||||
|               ), | ||||
|             ), | ||||
| @@ -260,7 +277,10 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border( | ||||
|                       top: BorderSide( | ||||
|                     color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8), | ||||
|                     color: Theme.of(context) | ||||
|                         .colorScheme | ||||
|                         .surfaceVariant | ||||
|                         .withOpacity(0.8), | ||||
|                     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), | ||||
|                     itemBuilder: (BuildContext context, int 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 Padding( | ||||
|                         padding: const EdgeInsets.only(top: 8, left: 8), | ||||
|                         child: ClipRRect( | ||||
|                           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                           borderRadius: | ||||
|                               const BorderRadius.all(Radius.circular(8)), | ||||
|                           child: InteractiveParticipantWidget( | ||||
|                             isFixed: true, | ||||
|                             width: 120, | ||||
| @@ -107,7 +109,8 @@ class _ChatCallState extends State<ChatCall> { | ||||
|                             color: Theme.of(context).cardColor, | ||||
|                             participant: track, | ||||
|                             onTap: () { | ||||
|                               if (track.participant.sid != _call.focusTrack?.participant.sid) { | ||||
|                               if (track.participant.sid != | ||||
|                                   _call.focusTrack?.participant.sid) { | ||||
|                                 _call.changeFocusTrack(track); | ||||
|                               } | ||||
|                             }, | ||||
|   | ||||
| @@ -113,10 +113,13 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|           constraints: const BoxConstraints(maxWidth: 640), | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|               _isSubmitting | ||||
|                   ? const LinearProgressIndicator().animate().scaleX() | ||||
|                   : Container(), | ||||
|               ListTile( | ||||
|                 title: Text(AppLocalizations.of(context)!.chatChannelUsage), | ||||
|                 subtitle: Text(AppLocalizations.of(context)!.chatChannelUsageCaption), | ||||
|                 subtitle: | ||||
|                     Text(AppLocalizations.of(context)!.chatChannelUsageCaption), | ||||
|                 leading: const CircleAvatar( | ||||
|                   backgroundColor: Colors.teal, | ||||
|                   child: Icon(Icons.tag, color: Colors.white), | ||||
| @@ -124,7 +127,8 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|               ), | ||||
|               const Divider(thickness: 0.3), | ||||
|               Container( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(horizontal: 16, vertical: 2), | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     Expanded( | ||||
| @@ -132,15 +136,18 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|                         autofocus: true, | ||||
|                         controller: _aliasController, | ||||
|                         decoration: InputDecoration.collapsed( | ||||
|                           hintText: AppLocalizations.of(context)!.chatChannelAliasLabel, | ||||
|                           hintText: AppLocalizations.of(context)! | ||||
|                               .chatChannelAliasLabel, | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                         onTapOutside: (_) => | ||||
|                             FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     TextButton( | ||||
|                       style: TextButton.styleFrom( | ||||
|                         shape: const CircleBorder(), | ||||
|                         visualDensity: const VisualDensity(horizontal: -2, vertical: -2), | ||||
|                         visualDensity: | ||||
|                             const VisualDensity(horizontal: -2, vertical: -2), | ||||
|                       ), | ||||
|                       onPressed: () => randomizeAlias(), | ||||
|                       child: const Icon(Icons.refresh), | ||||
| @@ -150,20 +157,24 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|               ), | ||||
|               const Divider(thickness: 0.3), | ||||
|               Container( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                 padding: | ||||
|                     const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                 child: TextField( | ||||
|                   autocorrect: true, | ||||
|                   controller: _nameController, | ||||
|                   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), | ||||
|               Expanded( | ||||
|                 child: Container( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|                   padding: | ||||
|                       const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|                   child: TextField( | ||||
|                     minLines: 5, | ||||
|                     maxLines: null, | ||||
| @@ -171,9 +182,11 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|                     keyboardType: TextInputType.multiline, | ||||
|                     controller: _descriptionController, | ||||
|                     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']; | ||||
|  | ||||
|     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); | ||||
|     if (res.statusCode == 200) { | ||||
| @@ -59,7 +60,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|       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( | ||||
|       uri, | ||||
| @@ -89,7 +91,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|       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( | ||||
|       uri, | ||||
| @@ -153,7 +156,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|             SliverToBoxAdapter( | ||||
|               child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|               child: _isSubmitting | ||||
|                   ? const LinearProgressIndicator().animate().scaleX() | ||||
|                   : Container(), | ||||
|             ), | ||||
|             SliverList.builder( | ||||
|               itemCount: _members.length, | ||||
| @@ -164,7 +169,9 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|  | ||||
|                 return Dismissible( | ||||
|                   key: Key(randomId.toString()), | ||||
|                   direction: getKickable(element) ? DismissDirection.startToEnd : DismissDirection.none, | ||||
|                   direction: getKickable(element) | ||||
|                       ? DismissDirection.startToEnd | ||||
|                       : DismissDirection.none, | ||||
|                   background: Container( | ||||
|                     color: Colors.red, | ||||
|                     padding: const EdgeInsets.symmetric(horizontal: 20), | ||||
| @@ -172,7 +179,8 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> { | ||||
|                     child: const Icon(Icons.remove, color: Colors.white), | ||||
|                   ), | ||||
|                   child: ListTile( | ||||
|                     leading: AccountAvatar(source: element.account.avatar, direct: true), | ||||
|                     leading: AccountAvatar( | ||||
|                         source: element.account.avatar, direct: true), | ||||
|                     title: Text(element.account.nick), | ||||
|                     subtitle: Text(element.account.name), | ||||
|                   ), | ||||
|   | ||||
| @@ -34,7 +34,8 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|   Call? _ongoingCall; | ||||
|   Channel? _channelMeta; | ||||
|  | ||||
|   final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); | ||||
|   final PagingController<int, Message> _pagingController = | ||||
|       PagingController(firstPageKey: 0); | ||||
|  | ||||
|   final http.Client _client = http.Client(); | ||||
|  | ||||
| @@ -53,7 +54,8 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|   } | ||||
|  | ||||
|   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); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)); | ||||
| @@ -82,8 +84,10 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|  | ||||
|     var res = await auth.client!.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|       final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); | ||||
|       final result = | ||||
|           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; | ||||
|       if (isLastPage || result.data == null) { | ||||
|         _pagingController.appendLastPage(items); | ||||
| @@ -97,7 +101,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|   } | ||||
|  | ||||
|   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.senderId != b.senderId) return false; | ||||
|     return a.createdAt.difference(b.createdAt).inMinutes <= 5; | ||||
| @@ -111,13 +115,16 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|  | ||||
|   void updateMessage(Message item) { | ||||
|     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) { | ||||
|     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(); | ||||
|     }); | ||||
|  | ||||
|     _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); | ||||
|     _pagingController | ||||
|         .addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
| @@ -157,10 +165,12 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|     Widget chatHistoryBuilder(context, item, index) { | ||||
|       bool isMerged = false, hasMerged = false; | ||||
|       if (index > 0) { | ||||
|         hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); | ||||
|         hasMerged = | ||||
|             getMessageMergeable(_pagingController.itemList?[index - 1], item); | ||||
|       } | ||||
|       if (index + 1 < (_pagingController.itemList?.length ?? 0)) { | ||||
|         isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); | ||||
|         isMerged = | ||||
|             getMessageMergeable(item, _pagingController.itemList?[index + 1]); | ||||
|       } | ||||
|       return InkWell( | ||||
|         child: Container( | ||||
| @@ -183,7 +193,8 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|     final callBanner = MaterialBanner( | ||||
|       padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), | ||||
|       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), | ||||
|       content: Text(AppLocalizations.of(context)!.chatCallOngoing), | ||||
|       actions: [ | ||||
| @@ -205,8 +216,12 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       title: _channelMeta?.name ?? "Loading...", | ||||
|       appBarActions: _channelMeta != null | ||||
|           ? [ | ||||
|               ChannelCallAction(call: _ongoingCall, channel: _channelMeta!, onUpdate: () => fetchMetadata()), | ||||
|               ChannelManageAction(channel: _channelMeta!, onUpdate: () => fetchMetadata()), | ||||
|               ChannelCallAction( | ||||
|                   call: _ongoingCall, | ||||
|                   channel: _channelMeta!, | ||||
|                   onUpdate: () => fetchMetadata()), | ||||
|               ChannelManageAction( | ||||
|                   channel: _channelMeta!, onUpdate: () => fetchMetadata()), | ||||
|             ] | ||||
|           : [], | ||||
|       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), | ||||
|   | ||||
| @@ -105,7 +105,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> { | ||||
|                           'channel': element.alias, | ||||
|                         }, | ||||
|                       ); | ||||
|                       switch(result) { | ||||
|                       switch (result) { | ||||
|                         case 'refresh': | ||||
|                           fetchChannels(); | ||||
|                       } | ||||
|   | ||||
| @@ -53,7 +53,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | ||||
|         leading: const Icon(Icons.settings), | ||||
|         title: Text(AppLocalizations.of(context)!.settings), | ||||
|         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 (router.canPop()) router.pop('refresh'); | ||||
|             } | ||||
| @@ -79,10 +81,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | ||||
|                 ), | ||||
|                 const SizedBox(width: 16), | ||||
|                 Expanded( | ||||
|                   child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ | ||||
|                     Text(widget.channel.name, style: Theme.of(context).textTheme.bodyLarge), | ||||
|                     Text(widget.channel.description, style: Theme.of(context).textTheme.bodySmall), | ||||
|                   ]), | ||||
|                   child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       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()), | ||||
|                 const Divider(thickness: 0.3), | ||||
|                 ListTile( | ||||
|                   leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app), | ||||
|                   title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit), | ||||
|                   leading: _isOwned | ||||
|                       ? const Icon(Icons.delete) | ||||
|                       : const Icon(Icons.exit_to_app), | ||||
|                   title: Text(_isOwned | ||||
|                       ? AppLocalizations.of(context)!.delete | ||||
|                       : AppLocalizations.of(context)!.exit), | ||||
|                   onTap: () => promptLeaveChannel(), | ||||
|                 ), | ||||
|               ], | ||||
|   | ||||
| @@ -22,7 +22,8 @@ class ExploreScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| 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(); | ||||
|  | ||||
| @@ -30,12 +31,15 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|     final offset = pageKey; | ||||
|     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); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|       final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty(); | ||||
|       final result = | ||||
|           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; | ||||
|       if (isLastPage || result.data == null) { | ||||
|         _pagingController.appendLastPage(items); | ||||
|   | ||||
| @@ -41,7 +41,8 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                       child: ListTile( | ||||
|                         leading: const Icon(Icons.check), | ||||
|                         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 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; | ||||
|  | ||||
| @@ -92,7 +94,8 @@ class NotificationItem extends StatelessWidget { | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             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( | ||||
|                 "Links", | ||||
|                 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; | ||||
|  | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|   | ||||
| @@ -129,69 +129,68 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | ||||
|           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), | ||||
|         ), | ||||
|       ], | ||||
|       child: Center( | ||||
|         child: Container( | ||||
|           constraints: const BoxConstraints(maxWidth: 640), | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|               FutureBuilder( | ||||
|                 future: auth.getProfiles(), | ||||
|                 builder: (context, snapshot) { | ||||
|                   if (snapshot.hasData) { | ||||
|                     var userinfo = snapshot.data; | ||||
|                     return ListTile( | ||||
|                       title: Text(userinfo["nick"]), | ||||
|                       subtitle: Text( | ||||
|                         AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                       ), | ||||
|                       leading: AccountAvatar( | ||||
|                         source: userinfo["picture"], | ||||
|                         direct: true, | ||||
|                       ), | ||||
|                     ); | ||||
|                   } else { | ||||
|                     return Container(); | ||||
|                   } | ||||
|                 }, | ||||
|               ), | ||||
|               const Divider(thickness: 0.3), | ||||
|               Expanded( | ||||
|                 child: Container( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                   child: TextField( | ||||
|                     maxLines: null, | ||||
|                     autofocus: true, | ||||
|                     autocorrect: true, | ||||
|                     keyboardType: TextInputType.multiline, | ||||
|                     controller: _textController, | ||||
|                     decoration: InputDecoration.collapsed( | ||||
|                       hintText: AppLocalizations.of(context)!.postContentPlaceholder, | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           _isSubmitting | ||||
|               ? const LinearProgressIndicator().animate().scaleX() | ||||
|               : Container(), | ||||
|           FutureBuilder( | ||||
|             future: auth.getProfiles(), | ||||
|             builder: (context, snapshot) { | ||||
|               if (snapshot.hasData) { | ||||
|                 var userinfo = snapshot.data; | ||||
|                 return ListTile( | ||||
|                   title: Text(userinfo["nick"]), | ||||
|                   subtitle: Text( | ||||
|                     AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               widget.editing != null ? editingBanner : Container(), | ||||
|               Container( | ||||
|                 decoration: const BoxDecoration( | ||||
|                   border: Border( | ||||
|                     top: BorderSide(width: 0.3, color: Color(0xffdedede)), | ||||
|                   leading: AccountAvatar( | ||||
|                     source: userinfo["picture"], | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     TextButton( | ||||
|                       style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                       child: const Icon(Icons.camera_alt), | ||||
|                       onPressed: () => viewAttachments(context), | ||||
|                     ) | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|                 ); | ||||
|               } else { | ||||
|                 return Container(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|           const Divider(thickness: 0.3), | ||||
|           Expanded( | ||||
|             child: Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               child: TextField( | ||||
|                 maxLines: null, | ||||
|                 autofocus: true, | ||||
|                 autocorrect: true, | ||||
|                 keyboardType: TextInputType.multiline, | ||||
|                 controller: _textController, | ||||
|                 decoration: InputDecoration.collapsed( | ||||
|                   hintText: | ||||
|                       AppLocalizations.of(context)!.postContentPlaceholder, | ||||
|                 ), | ||||
|                 onTapOutside: (_) => | ||||
|                     FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           widget.editing != null ? editingBanner : Container(), | ||||
|           Container( | ||||
|             decoration: const BoxDecoration( | ||||
|               border: Border( | ||||
|                 top: BorderSide(width: 0.3, color: Color(0xffdedede)), | ||||
|               ), | ||||
|             ), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 TextButton( | ||||
|                   style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                   child: const Icon(Icons.camera_alt), | ||||
|                   onPressed: () => viewAttachments(context), | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -119,69 +119,68 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | ||||
|           child: Text(AppLocalizations.of(context)!.postVerb.toUpperCase()), | ||||
|         ), | ||||
|       ], | ||||
|       child: Center( | ||||
|         child: Container( | ||||
|           constraints: const BoxConstraints(maxWidth: 640), | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|               FutureBuilder( | ||||
|                 future: auth.getProfiles(), | ||||
|                 builder: (context, snapshot) { | ||||
|                   if (snapshot.hasData) { | ||||
|                     var userinfo = snapshot.data; | ||||
|                     return ListTile( | ||||
|                       title: Text(userinfo["nick"]), | ||||
|                       subtitle: Text( | ||||
|                         AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                       ), | ||||
|                       leading: AccountAvatar( | ||||
|                         source: userinfo["picture"], | ||||
|                         direct: true, | ||||
|                       ), | ||||
|                     ); | ||||
|                   } else { | ||||
|                     return Container(); | ||||
|                   } | ||||
|                 }, | ||||
|               ), | ||||
|               const Divider(thickness: 0.3), | ||||
|               Expanded( | ||||
|                 child: Container( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                   child: TextField( | ||||
|                     maxLines: null, | ||||
|                     autofocus: true, | ||||
|                     autocorrect: true, | ||||
|                     keyboardType: TextInputType.multiline, | ||||
|                     controller: _textController, | ||||
|                     decoration: InputDecoration.collapsed( | ||||
|                       hintText: AppLocalizations.of(context)!.postContentPlaceholder, | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           _isSubmitting | ||||
|               ? const LinearProgressIndicator().animate().scaleX() | ||||
|               : Container(), | ||||
|           FutureBuilder( | ||||
|             future: auth.getProfiles(), | ||||
|             builder: (context, snapshot) { | ||||
|               if (snapshot.hasData) { | ||||
|                 var userinfo = snapshot.data; | ||||
|                 return ListTile( | ||||
|                   title: Text(userinfo["nick"]), | ||||
|                   subtitle: Text( | ||||
|                     AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               widget.editing != null ? editingBanner : Container(), | ||||
|               Container( | ||||
|                 decoration: const BoxDecoration( | ||||
|                   border: Border( | ||||
|                     top: BorderSide(width: 0.3, color: Color(0xffdedede)), | ||||
|                   leading: AccountAvatar( | ||||
|                     source: userinfo["picture"], | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     TextButton( | ||||
|                       style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                       child: const Icon(Icons.camera_alt), | ||||
|                       onPressed: () => viewAttachments(context), | ||||
|                     ) | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|                 ); | ||||
|               } else { | ||||
|                 return Container(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|           const Divider(thickness: 0.3), | ||||
|           Expanded( | ||||
|             child: Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               child: TextField( | ||||
|                 maxLines: null, | ||||
|                 autofocus: true, | ||||
|                 autocorrect: true, | ||||
|                 keyboardType: TextInputType.multiline, | ||||
|                 controller: _textController, | ||||
|                 decoration: InputDecoration.collapsed( | ||||
|                   hintText: | ||||
|                       AppLocalizations.of(context)!.postContentPlaceholder, | ||||
|                 ), | ||||
|                 onTapOutside: (_) => | ||||
|                     FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           widget.editing != null ? editingBanner : Container(), | ||||
|           Container( | ||||
|             decoration: const BoxDecoration( | ||||
|               border: Border( | ||||
|                 top: BorderSide(width: 0.3, color: Color(0xffdedede)), | ||||
|               ), | ||||
|             ), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 TextButton( | ||||
|                   style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                   child: const Icon(Icons.camera_alt), | ||||
|                   onPressed: () => viewAttachments(context), | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -24,10 +24,12 @@ class PostScreen extends StatefulWidget { | ||||
| class _PostScreenState extends State<PostScreen> { | ||||
|   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 { | ||||
|     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); | ||||
|     if (res.statusCode != 200) { | ||||
|       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, | ||||
|       children: [ | ||||
|         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( | ||||
|             AppLocalizations.of(context)!.friend, | ||||
|             style: Theme.of(context).textTheme.headlineSmall, | ||||
|   | ||||
| @@ -41,7 +41,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     participant.addListener(onChange); | ||||
|     _subscription = Hardware.instance.onDeviceChange.stream.listen((List<MediaDevice> devices) { | ||||
|     _subscription = Hardware.instance.onDeviceChange.stream | ||||
|         .listen((List<MediaDevice> devices) { | ||||
|       revertDevices(devices); | ||||
|     }); | ||||
|     Hardware.instance.enumerateDevices().then(revertDevices); | ||||
| @@ -161,18 +162,23 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|           if (!isRetry) { | ||||
|             const androidConfig = FlutterBackgroundAndroidConfig( | ||||
|               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, | ||||
|               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(); | ||||
|           } | ||||
|         } catch (e) { | ||||
|           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, | ||||
|         children: [ | ||||
|           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, | ||||
|             onPressed: disconnect, | ||||
|           ), | ||||
| @@ -253,7 +260,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|                         return PopupMenuItem<MediaDevice>( | ||||
|                           value: device, | ||||
|                           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_outline_blank), | ||||
|                             title: Text(device.label), | ||||
| @@ -281,7 +289,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|                     onTap: disableVideo, | ||||
|                     child: ListTile( | ||||
|                       leading: const Icon(Icons.videocam_off), | ||||
|                       title: Text(AppLocalizations.of(context)!.chatCallVideoOff), | ||||
|                       title: | ||||
|                           Text(AppLocalizations.of(context)!.chatCallVideoOff), | ||||
|                     ), | ||||
|                   ), | ||||
|                   if (_videoInputs != null) | ||||
| @@ -289,7 +298,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|                       return PopupMenuItem<MediaDevice>( | ||||
|                         value: device, | ||||
|                         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_outline_blank), | ||||
|                           title: Text(device.label), | ||||
| @@ -308,7 +318,9 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|               tooltip: AppLocalizations.of(context)!.chatCallVideoOn, | ||||
|             ), | ||||
|           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, | ||||
|             onPressed: () => toggleCamera(), | ||||
|             tooltip: AppLocalizations.of(context)!.chatCallVideoFlip, | ||||
| @@ -330,7 +342,8 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|                       return PopupMenuItem<MediaDevice>( | ||||
|                         value: device, | ||||
|                         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_outline_blank), | ||||
|                           title: Text(device.label), | ||||
| @@ -343,9 +356,12 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|             ), | ||||
|           if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) | ||||
|             IconButton( | ||||
|               onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null, | ||||
|               onPressed: Hardware.instance.canSwitchSpeakerphone | ||||
|                   ? setSpeakerphoneOn | ||||
|                   : null, | ||||
|               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, | ||||
|             ), | ||||
|           if (participant.isScreenShareEnabled()) | ||||
|   | ||||
| @@ -20,7 +20,8 @@ class NoContentWidget extends StatefulWidget { | ||||
|   State<NoContentWidget> createState() => _NoContentWidgetState(); | ||||
| } | ||||
|  | ||||
| class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProviderStateMixin { | ||||
| class _NoContentWidgetState extends State<NoContentWidget> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final AnimationController _animationController; | ||||
|  | ||||
|   @override | ||||
| @@ -35,7 +36,9 @@ class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProv | ||||
|     if (widget.isSpeaking) { | ||||
|       _animationController.repeat(reverse: true); | ||||
|     } 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( | ||||
|                 decoration: BoxDecoration( | ||||
|                   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, | ||||
|               ), | ||||
|   | ||||
| @@ -95,7 +95,8 @@ class RemoteParticipantWidget extends ParticipantWidget { | ||||
|   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; | ||||
|  | ||||
|   TrackPublication? get _firstAudioPublication; | ||||
| @@ -126,7 +127,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat | ||||
|   void onParticipantChanged() { | ||||
|     setState(() { | ||||
|       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, | ||||
|             children: [ | ||||
|               ParticipantInfoWidget( | ||||
|                 title: widget.participant.name.isNotEmpty ? widget.participant.name : widget.participant.identity, | ||||
|                 audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true, | ||||
|                 title: widget.participant.name.isNotEmpty | ||||
|                     ? widget.participant.name | ||||
|                     : widget.participant.identity, | ||||
|                 audioAvailable: _firstAudioPublication?.muted == false && | ||||
|                     _firstAudioPublication?.subscribed == true, | ||||
|                 connectionQuality: widget.participant.connectionQuality, | ||||
|                 isScreenShare: widget.isScreenShare, | ||||
|               ), | ||||
| @@ -171,7 +176,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> { | ||||
| class _LocalParticipantWidgetState | ||||
|     extends _ParticipantWidgetState<LocalParticipantWidget> { | ||||
|   @override | ||||
|   LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication => | ||||
|       widget.participant.audioTrackPublications.firstOrNull; | ||||
| @@ -180,7 +186,8 @@ class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticip | ||||
|   VideoTrack? get _activeVideoTrack => widget.videoTrack; | ||||
| } | ||||
|  | ||||
| class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> { | ||||
| class _RemoteParticipantWidgetState | ||||
|     extends _ParticipantWidgetState<RemoteParticipantWidget> { | ||||
|   @override | ||||
|   RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication => | ||||
|       widget.participant.audioTrackPublications.firstOrNull; | ||||
|   | ||||
| @@ -55,7 +55,9 @@ class ParticipantInfoWidget extends StatelessWidget { | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(left: 5), | ||||
|                 child: Icon( | ||||
|                   connectionQuality == ConnectionQuality.poor ? Icons.wifi_off_outlined : Icons.wifi, | ||||
|                   connectionQuality == ConnectionQuality.poor | ||||
|                       ? Icons.wifi_off_outlined | ||||
|                       : Icons.wifi, | ||||
|                   color: { | ||||
|                     ConnectionQuality.excellent: Colors.green, | ||||
|                     ConnectionQuality.good: Colors.orange, | ||||
|   | ||||
| @@ -22,7 +22,9 @@ class ParticipantMenu extends StatefulWidget { | ||||
|  | ||||
| class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|   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 => | ||||
|       widget.participant.audioTrackPublications.firstOrNull; | ||||
| @@ -39,7 +41,8 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         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( | ||||
|             padding: const EdgeInsets.symmetric( | ||||
|               horizontal: 8, | ||||
| @@ -59,9 +62,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|                   leading: Icon( | ||||
|                     Icons.volume_up, | ||||
|                     color: { | ||||
|                       TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, | ||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), | ||||
|                       TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, | ||||
|                       TrackSubscriptionState.notAllowed: | ||||
|                           Theme.of(context).colorScheme.error, | ||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context) | ||||
|                           .colorScheme | ||||
|                           .onSurface | ||||
|                           .withOpacity(0.6), | ||||
|                       TrackSubscriptionState.subscribed: | ||||
|                           Theme.of(context).colorScheme.primary, | ||||
|                     }[_firstAudioPublication!.subscriptionState], | ||||
|                   ), | ||||
|                   title: Text( | ||||
| @@ -83,9 +91,14 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|                   leading: Icon( | ||||
|                     widget.isScreenShare ? Icons.monitor : Icons.videocam, | ||||
|                     color: { | ||||
|                       TrackSubscriptionState.notAllowed: Theme.of(context).colorScheme.error, | ||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), | ||||
|                       TrackSubscriptionState.subscribed: Theme.of(context).colorScheme.primary, | ||||
|                       TrackSubscriptionState.notAllowed: | ||||
|                           Theme.of(context).colorScheme.error, | ||||
|                       TrackSubscriptionState.unsubscribed: Theme.of(context) | ||||
|                           .colorScheme | ||||
|                           .onSurface | ||||
|                           .withOpacity(0.6), | ||||
|                       TrackSubscriptionState.subscribed: | ||||
|                           Theme.of(context).colorScheme.primary, | ||||
|                     }[_videoPublication!.subscriptionState], | ||||
|                   ), | ||||
|                   title: Text( | ||||
| @@ -107,7 +120,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|                 ...[30, 15, 8].map( | ||||
|                   (x) => ListTile( | ||||
|                     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'), | ||||
|                     onTap: () { | ||||
| @@ -125,7 +140,9 @@ class _ParticipantMenuState extends State<ParticipantMenu> { | ||||
|                 ].map( | ||||
|                   (x) => ListTile( | ||||
|                     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}'), | ||||
|                     onTap: () { | ||||
|   | ||||
| @@ -28,11 +28,14 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | ||||
|             stats['layer-$key'] = | ||||
|                 '${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) { | ||||
|             stats['encoder'] = firstStats.encoderImplementation ?? ''; | ||||
|             stats['video codec'] = '${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}'; | ||||
|             stats['qualityLimitationReason'] = firstStats.qualityLimitationReason ?? ''; | ||||
|             stats['video codec'] = | ||||
|                 '${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) { | ||||
|         setState(() { | ||||
|           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'] = | ||||
|               '${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps'; | ||||
|           stats['video jitter'] = '${event.stats.jitter} s'; | ||||
| @@ -70,7 +74,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | ||||
|           stats['audio codec'] = | ||||
|               '${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}'; | ||||
|           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 received'] = '${event.stats.packetsReceived}'; | ||||
|         }); | ||||
| @@ -83,7 +88,10 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | ||||
|       element.dispose(); | ||||
|     } | ||||
|     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) { | ||||
|         _setUpListener(track.track!); | ||||
|       } | ||||
| @@ -117,7 +125,8 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> { | ||||
|         horizontal: 8, | ||||
|       ), | ||||
|       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 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 | ||||
|   State<ChannelCallAction> createState() => _ChannelCallActionState(); | ||||
| @@ -32,7 +33,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | ||||
|       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); | ||||
|     if (res.statusCode != 200) { | ||||
| @@ -52,7 +54,8 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | ||||
|       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); | ||||
|     if (res.statusCode != 200) { | ||||
| @@ -75,7 +78,9 @@ class _ChannelCallActionState extends State<ChannelCallAction> { | ||||
|                 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 Function onUpdate; | ||||
|  | ||||
|   const ChannelManageAction({super.key, required this.channel, required this.onUpdate}); | ||||
|   const ChannelManageAction( | ||||
|       {super.key, required this.channel, required this.onUpdate}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -12,7 +12,8 @@ class ChannelDeletion extends StatefulWidget { | ||||
|   final Channel channel; | ||||
|   final bool isOwned; | ||||
|  | ||||
|   const ChannelDeletion({super.key, required this.channel, required this.isOwned}); | ||||
|   const ChannelDeletion( | ||||
|       {super.key, required this.channel, required this.isOwned}); | ||||
|  | ||||
|   @override | ||||
|   State<ChannelDeletion> createState() => _ChannelDeletionState(); | ||||
|   | ||||
| @@ -55,19 +55,23 @@ class _ChatMaintainerState extends State<ChatMaintainer> { | ||||
|           switch (result.method) { | ||||
|             case 'messages.new': | ||||
|               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; | ||||
|             case 'messages.update': | ||||
|               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; | ||||
|             case 'messages.burnt': | ||||
|               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; | ||||
|             case 'calls.new': | ||||
|               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; | ||||
|             case 'calls.end': | ||||
|               final payload = Call.fromJson(result.payload!); | ||||
|   | ||||
| @@ -79,7 +79,9 @@ class ChatMessageAction extends StatelessWidget { | ||||
|  | ||||
|                   return ListView( | ||||
|                     children: [ | ||||
|                       ...(snapshot.data['id'] == item.sender.account.externalId ? authorizedItems : List.empty()), | ||||
|                       ...(snapshot.data['id'] == item.sender.account.externalId | ||||
|                           ? authorizedItems | ||||
|                           : List.empty()), | ||||
|                       ListTile( | ||||
|                         leading: const Icon(Icons.reply), | ||||
|                         title: Text(AppLocalizations.of(context)!.reply), | ||||
|   | ||||
| @@ -19,7 +19,8 @@ class ChatMessageDeletionDialog extends StatefulWidget { | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<ChatMessageDeletionDialog> createState() => _ChatMessageDeletionDialogState(); | ||||
|   State<ChatMessageDeletionDialog> createState() => | ||||
|       _ChatMessageDeletionDialogState(); | ||||
| } | ||||
|  | ||||
| class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { | ||||
| @@ -29,7 +30,8 @@ class _ChatMessageDeletionDialogState extends State<ChatMessageDeletionDialog> { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     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); | ||||
|     final res = await auth.client!.delete(uri); | ||||
|   | ||||
| @@ -18,7 +18,12 @@ class ChatMessageEditor extends StatefulWidget { | ||||
|   final Message? replying; | ||||
|   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 | ||||
|   State<ChatMessageEditor> createState() => _ChatMessageEditorState(); | ||||
| @@ -51,7 +56,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|  | ||||
|     final uri = widget.editing == null | ||||
|         ? 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); | ||||
|     req.headers['Content-Type'] = 'application/json'; | ||||
| @@ -84,7 +90,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|       setState(() { | ||||
|         _prevEditingId = widget.editing!.id; | ||||
|         _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: [ | ||||
|               badge.Badge( | ||||
|                 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), | ||||
|                 child: TextButton( | ||||
|                   style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), | ||||
|                   onPressed: !_isSubmitting ? () => viewAttachments(context) : null, | ||||
|                   style: TextButton.styleFrom( | ||||
|                       shape: const CircleBorder(), | ||||
|                       padding: const EdgeInsets.all(4)), | ||||
|                   onPressed: | ||||
|                       !_isSubmitting ? () => viewAttachments(context) : null, | ||||
|                   child: const Icon(Icons.attach_file), | ||||
|                 ), | ||||
|               ), | ||||
| @@ -163,14 +174,18 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|                   autocorrect: true, | ||||
|                   keyboardType: TextInputType.text, | ||||
|                   decoration: InputDecoration.collapsed( | ||||
|                     hintText: AppLocalizations.of(context)!.chatMessagePlaceholder, | ||||
|                     hintText: | ||||
|                         AppLocalizations.of(context)!.chatMessagePlaceholder, | ||||
|                   ), | ||||
|                   onSubmitted: (_) => sendMessage(context), | ||||
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|               ), | ||||
|               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, | ||||
|                 child: const Icon(Icons.send), | ||||
|               ) | ||||
|   | ||||
| @@ -8,7 +8,8 @@ extension SolianCommonExtensions on BuildContext { | ||||
|       if (message.trim().isEmpty) return ''; | ||||
|       return message | ||||
|           .split(' ') | ||||
|           .map((element) => "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") | ||||
|           .map((element) => | ||||
|               "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") | ||||
|           .join(" "); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -22,10 +22,12 @@ class IndentWrapper extends LayoutWrapper { | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: hideDrawer ? IconButton( | ||||
|           icon: const Icon(Icons.arrow_back), | ||||
|           onPressed: () => router.pop(), | ||||
|         ) : null, | ||||
|         leading: hideDrawer | ||||
|             ? IconButton( | ||||
|                 icon: const Icon(Icons.arrow_back), | ||||
|                 onPressed: () => router.pop(), | ||||
|               ) | ||||
|             : null, | ||||
|         title: Text(title), | ||||
|         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>(); | ||||
|     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>(); | ||||
|     if (!await auth.isAuthorized()) return; | ||||
|  | ||||
| @@ -102,7 +104,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|   Future<void> uploadAttachment(File file, String hashcode) async { | ||||
|     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.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 req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}')); | ||||
|     final req = MultipartRequest('DELETE', | ||||
|         getRequestUri(widget.provider, '/api/attachments/${item.id}')); | ||||
|  | ||||
|     setState(() => _isSubmitting = true); | ||||
|     var res = await auth.client!.send(req); | ||||
| @@ -162,7 +167,17 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|     if (bytes == 0) return '0 Bytes'; | ||||
|     const k = 1024; | ||||
|     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(); | ||||
|     return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; | ||||
|   } | ||||
| @@ -180,7 +195,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|     return Column( | ||||
|       children: [ | ||||
|         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( | ||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|             children: [ | ||||
| @@ -199,7 +215,9 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|                 builder: (context, snapshot) { | ||||
|                   if (snapshot.hasData && snapshot.data == true) { | ||||
|                     return TextButton( | ||||
|                       onPressed: _isSubmitting ? null : () => viewAttachMethods(context), | ||||
|                       onPressed: _isSubmitting | ||||
|                           ? null | ||||
|                           : () => viewAttachMethods(context), | ||||
|                       style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                       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( | ||||
|           child: ListView.separated( | ||||
|             itemCount: _attachments.length, | ||||
| @@ -243,7 +263,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|                         foregroundColor: Colors.red, | ||||
|                       ), | ||||
|                       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( | ||||
|                       mainAxisSize: MainAxisSize.min, | ||||
|                       children: [ | ||||
|                         const Icon(Icons.add_photo_alternate, color: Colors.indigo), | ||||
|                         const Icon(Icons.add_photo_alternate, | ||||
|                             color: Colors.indigo), | ||||
|                         const SizedBox(height: 8), | ||||
|                         Text(AppLocalizations.of(context)!.pickPhoto), | ||||
|                       ], | ||||
|   | ||||
| @@ -36,6 +36,17 @@ class _AttachmentItemState extends State<AttachmentItem> { | ||||
|   ); | ||||
|   late final _videoController = VideoController(_videoPlayer); | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if (widget.type != 1) { | ||||
|       _videoPlayer.open( | ||||
|         Media(widget.url), | ||||
|         play: false, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     const borderRadius = Radius.circular(8); | ||||
| @@ -53,6 +64,7 @@ class _AttachmentItemState extends State<AttachmentItem> { | ||||
|               children: [ | ||||
|                 Image.network( | ||||
|                   widget.url, | ||||
|                   key: Key(getTag()), | ||||
|                   width: double.infinity, | ||||
|                   height: double.infinity, | ||||
|                   fit: BoxFit.cover, | ||||
| @@ -63,6 +75,7 @@ class _AttachmentItemState extends State<AttachmentItem> { | ||||
|                         right: 12, | ||||
|                         bottom: 8, | ||||
|                         child: Material( | ||||
|                           color: Colors.transparent, | ||||
|                           child: Chip(label: Text(widget.badge!)), | ||||
|                         ), | ||||
|                       ) | ||||
| @@ -83,11 +96,6 @@ class _AttachmentItemState extends State<AttachmentItem> { | ||||
|         }, | ||||
|       ); | ||||
|     } else { | ||||
|       _videoPlayer.open( | ||||
|         Media(widget.url), | ||||
|         play: false, | ||||
|       ); | ||||
|  | ||||
|       content = ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(borderRadius), | ||||
|         child: Video( | ||||
| @@ -121,9 +129,11 @@ class AttachmentList extends StatelessWidget { | ||||
|   final List<Attachment> items; | ||||
|   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 | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -47,7 +47,8 @@ class _PostItemState extends State<PostItem> { | ||||
|   } | ||||
|  | ||||
|   void viewComments() { | ||||
|     final PagingController<int, Post> commentPaging = PagingController(firstPageKey: 0); | ||||
|     final PagingController<int, Post> commentPaging = | ||||
|         PagingController(firstPageKey: 0); | ||||
|  | ||||
|     showModalBottomSheet( | ||||
|       context: context, | ||||
| @@ -87,10 +88,12 @@ class _PostItemState extends State<PostItem> { | ||||
|   Widget renderAttachments() { | ||||
|     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( | ||||
|         padding: const EdgeInsets.only(top: 8), | ||||
|         child: AttachmentList(items: widget.item.attachments!, provider: 'interactive'), | ||||
|         child: AttachmentList( | ||||
|             items: widget.item.attachments!, provider: 'interactive'), | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(); | ||||
| @@ -130,8 +133,9 @@ class _PostItemState extends State<PostItem> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String getAuthorDescribe() => | ||||
|       widget.item.author.description.isNotEmpty ? widget.item.author.description : 'No description yet.'; | ||||
|   String getAuthorDescribe() => widget.item.author.description.isNotEmpty | ||||
|       ? widget.item.author.description | ||||
|       : 'No description yet.'; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
| @@ -177,7 +181,8 @@ class _PostItemState extends State<PostItem> { | ||||
|                     children: [ | ||||
|                       ...headingParts, | ||||
|                       Padding( | ||||
|                         padding: const EdgeInsets.only(left: 12, right: 12, top: 4), | ||||
|                         padding: | ||||
|                             const EdgeInsets.only(left: 12, right: 12, top: 4), | ||||
|                         child: renderContent(), | ||||
|                       ), | ||||
|                       renderAttachments(), | ||||
|   | ||||
| @@ -106,7 +106,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> { | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         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( | ||||
|             padding: const EdgeInsets.symmetric( | ||||
|               horizontal: 8, | ||||
| @@ -118,7 +119,9 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> { | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), | ||||
|         _isSubmitting | ||||
|             ? const LinearProgressIndicator().animate().scaleX() | ||||
|             : Container(), | ||||
|         Expanded( | ||||
|           child: ListView.builder( | ||||
|             itemCount: reactions.length, | ||||
|   | ||||
| @@ -36,5 +36,4 @@ class SignInRequiredScreen extends StatelessWidget { | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user