✨ Basic large screen support
This commit is contained in:
		| @@ -28,32 +28,32 @@ class Account { | ||||
|   }); | ||||
|  | ||||
|   factory Account.fromJson(Map<String, dynamic> json) => Account( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         name: json["name"], | ||||
|         nick: json["nick"], | ||||
|         avatar: json["avatar"], | ||||
|         banner: json["banner"], | ||||
|         description: json["description"], | ||||
|         emailAddress: json["email_address"], | ||||
|         powerLevel: json["power_level"], | ||||
|         externalId: json["external_id"], | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         name: json['name'], | ||||
|         nick: json['nick'], | ||||
|         avatar: json['avatar'], | ||||
|         banner: json['banner'], | ||||
|         description: json['description'], | ||||
|         emailAddress: json['email_address'], | ||||
|         powerLevel: json['power_level'], | ||||
|         externalId: json['external_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "name": name, | ||||
|         "nick": nick, | ||||
|         "avatar": avatar, | ||||
|         "banner": banner, | ||||
|         "description": description, | ||||
|         "email_address": emailAddress, | ||||
|         "power_level": powerLevel, | ||||
|         "external_id": externalId, | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'name': name, | ||||
|         'nick': nick, | ||||
|         'avatar': avatar, | ||||
|         'banner': banner, | ||||
|         'description': description, | ||||
|         'email_address': emailAddress, | ||||
|         'power_level': powerLevel, | ||||
|         'external_id': externalId, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -28,32 +28,32 @@ class Author { | ||||
|   }); | ||||
|  | ||||
|   factory Author.fromJson(Map<String, dynamic> json) => Author( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         name: json["name"], | ||||
|         nick: json["nick"], | ||||
|         avatar: json["avatar"], | ||||
|         banner: json["banner"], | ||||
|         description: json["description"], | ||||
|         emailAddress: json["email_address"], | ||||
|         powerLevel: json["power_level"], | ||||
|         externalId: json["external_id"], | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         name: json['name'], | ||||
|         nick: json['nick'], | ||||
|         avatar: json['avatar'], | ||||
|         banner: json['banner'], | ||||
|         description: json['description'], | ||||
|         emailAddress: json['email_address'], | ||||
|         powerLevel: json['power_level'], | ||||
|         externalId: json['external_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "name": name, | ||||
|         "nick": nick, | ||||
|         "avatar": avatar, | ||||
|         "banner": banner, | ||||
|         "description": description, | ||||
|         "email_address": emailAddress, | ||||
|         "power_level": powerLevel, | ||||
|         "external_id": externalId, | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'name': name, | ||||
|         'nick': nick, | ||||
|         'avatar': avatar, | ||||
|         'banner': banner, | ||||
|         'description': description, | ||||
|         'email_address': emailAddress, | ||||
|         'power_level': powerLevel, | ||||
|         'external_id': externalId, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -25,28 +25,28 @@ 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"], | ||||
|         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"]), | ||||
|             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(), | ||||
|       }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -32,35 +32,35 @@ 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, | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -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,29 +26,29 @@ 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) { | ||||
|   | ||||
| @@ -36,42 +36,42 @@ class Message { | ||||
|   }); | ||||
|  | ||||
|   factory Message.fromJson(Map<String, dynamic> json) => Message( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         content: json["content"], | ||||
|         metadata: json["metadata"], | ||||
|         type: json["type"], | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         content: json['content'], | ||||
|         metadata: json['metadata'], | ||||
|         type: json['type'], | ||||
|         attachments: List<Attachment>.from( | ||||
|             json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? | ||||
|             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"]) | ||||
|         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, | ||||
|         channelId: json["channel_id"], | ||||
|         senderId: json["sender_id"], | ||||
|         channelId: json['channel_id'], | ||||
|         senderId: json['sender_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "content": content, | ||||
|         "metadata": metadata, | ||||
|         "type": type, | ||||
|         "attachments": List<dynamic>.from( | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'content': content, | ||||
|         'metadata': metadata, | ||||
|         'type': type, | ||||
|         'attachments': List<dynamic>.from( | ||||
|             attachments?.map((x) => x.toJson()) ?? List.empty()), | ||||
|         "channel": channel?.toJson(), | ||||
|         "sender": sender.toJson(), | ||||
|         "reply_id": replyId, | ||||
|         "reply_to": replyTo?.toJson(), | ||||
|         "channel_id": channelId, | ||||
|         "sender_id": senderId, | ||||
|         'channel': channel?.toJson(), | ||||
|         'sender': sender.toJson(), | ||||
|         'reply_id': replyId, | ||||
|         'reply_to': replyTo?.toJson(), | ||||
|         'channel_id': channelId, | ||||
|         'sender_id': senderId, | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -97,24 +97,24 @@ class Sender { | ||||
|   }); | ||||
|  | ||||
|   factory Sender.fromJson(Map<String, dynamic> json) => Sender( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         account: Account.fromJson(json["account"]), | ||||
|         channelId: json["channel_id"], | ||||
|         accountId: json["account_id"], | ||||
|         notify: json["notify"], | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         account: Account.fromJson(json['account']), | ||||
|         channelId: json['channel_id'], | ||||
|         accountId: json['account_id'], | ||||
|         notify: json['notify'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "account": account.toJson(), | ||||
|         "channel_id": channelId, | ||||
|         "account_id": accountId, | ||||
|         "notify": notify, | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'account': account.toJson(), | ||||
|         'channel_id': channelId, | ||||
|         'account_id': accountId, | ||||
|         'notify': notify, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -28,37 +28,37 @@ class Notification { | ||||
|   }); | ||||
|  | ||||
|   factory Notification.fromJson(Map<String, dynamic> json) => Notification( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         subject: json["subject"], | ||||
|         content: json["content"], | ||||
|         links: json["links"] != null | ||||
|             ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         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(), | ||||
|         isImportant: json["is_important"], | ||||
|         isRealtime: json["is_realtime"], | ||||
|         readAt: json["read_at"], | ||||
|         senderId: json["sender_id"], | ||||
|         recipientId: json["recipient_id"], | ||||
|         isImportant: json['is_important'], | ||||
|         isRealtime: json['is_realtime'], | ||||
|         readAt: json['read_at'], | ||||
|         senderId: json['sender_id'], | ||||
|         recipientId: json['recipient_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "subject": subject, | ||||
|         "content": content, | ||||
|         "links": links != null | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'subject': subject, | ||||
|         'content': content, | ||||
|         'links': links != null | ||||
|             ? List<dynamic>.from(links!.map((x) => x.toJson())) | ||||
|             : List.empty(), | ||||
|         "is_important": isImportant, | ||||
|         "is_realtime": isRealtime, | ||||
|         "read_at": readAt, | ||||
|         "sender_id": senderId, | ||||
|         "recipient_id": recipientId, | ||||
|         'is_important': isImportant, | ||||
|         'is_realtime': isRealtime, | ||||
|         'read_at': readAt, | ||||
|         'sender_id': senderId, | ||||
|         'recipient_id': recipientId, | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -72,12 +72,12 @@ class Link { | ||||
|   }); | ||||
|  | ||||
|   factory Link.fromJson(Map<String, dynamic> json) => Link( | ||||
|         label: json["label"], | ||||
|         url: json["url"], | ||||
|         label: json['label'], | ||||
|         url: json['url'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "label": label, | ||||
|         "url": url, | ||||
|         'label': label, | ||||
|         'url': url, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -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,10 +8,10 @@ class PaginationResult { | ||||
|   }); | ||||
|  | ||||
|   factory PaginationResult.fromJson(Map<String, dynamic> json) => | ||||
|       PaginationResult(count: json["count"], data: json["data"]); | ||||
|       PaginationResult(count: json['count'], data: json['data']); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "count": count, | ||||
|         "data": data, | ||||
|         'count': count, | ||||
|         'data': data, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,8 @@ class Post { | ||||
|   List<Attachment>? attachments; | ||||
|   Map<String, dynamic>? reactionList; | ||||
|  | ||||
|   String get dataset => '${modelType}s'; | ||||
|  | ||||
|   Post({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
| @@ -38,46 +40,46 @@ class Post { | ||||
|   }); | ||||
|  | ||||
|   factory Post.fromJson(Map<String, dynamic> json) => Post( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         alias: json["alias"], | ||||
|         title: json["title"], | ||||
|         description: json["description"], | ||||
|         content: json["content"], | ||||
|         modelType: json["model_type"], | ||||
|         commentCount: json["comment_count"], | ||||
|         reactionCount: json["reaction_count"], | ||||
|         authorId: json["author_id"], | ||||
|         realmId: json["realm_id"], | ||||
|         author: Author.fromJson(json["author"]), | ||||
|         attachments: json["attachments"] != null | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         alias: json['alias'], | ||||
|         title: json['title'], | ||||
|         description: json['description'], | ||||
|         content: json['content'], | ||||
|         modelType: json['model_type'], | ||||
|         commentCount: json['comment_count'], | ||||
|         reactionCount: json['reaction_count'], | ||||
|         authorId: json['author_id'], | ||||
|         realmId: json['realm_id'], | ||||
|         author: Author.fromJson(json['author']), | ||||
|         attachments: json['attachments'] != null | ||||
|             ? List<Attachment>.from( | ||||
|                 json["attachments"].map((x) => Attachment.fromJson(x))) | ||||
|                 json['attachments'].map((x) => Attachment.fromJson(x))) | ||||
|             : List.empty(), | ||||
|         reactionList: json["reaction_list"], | ||||
|         reactionList: json['reaction_list'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "alias": alias, | ||||
|         "title": title, | ||||
|         "description": description, | ||||
|         "content": content, | ||||
|         "model_type": modelType, | ||||
|         "comment_count": commentCount, | ||||
|         "reaction_count": reactionCount, | ||||
|         "author_id": authorId, | ||||
|         "realm_id": realmId, | ||||
|         "author": author.toJson(), | ||||
|         "attachments": attachments == null | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'alias': alias, | ||||
|         'title': title, | ||||
|         'description': description, | ||||
|         'content': content, | ||||
|         'model_type': modelType, | ||||
|         'comment_count': commentCount, | ||||
|         'reaction_count': reactionCount, | ||||
|         'author_id': authorId, | ||||
|         'realm_id': realmId, | ||||
|         'author': author.toJson(), | ||||
|         'attachments': attachments == null | ||||
|             ? List.empty() | ||||
|             : List<dynamic>.from(attachments!.map((x) => x.toJson())), | ||||
|         "reaction_list": reactionList, | ||||
|         'reaction_list': reactionList, | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -117,38 +119,38 @@ class Attachment { | ||||
|   }); | ||||
|  | ||||
|   factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( | ||||
|         id: json["id"], | ||||
|         createdAt: DateTime.parse(json["created_at"]), | ||||
|         updatedAt: DateTime.parse(json["updated_at"]), | ||||
|         deletedAt: json["deleted_at"], | ||||
|         fileId: json["file_id"], | ||||
|         filesize: json["filesize"], | ||||
|         filename: json["filename"], | ||||
|         mimetype: json["mimetype"], | ||||
|         type: json["type"], | ||||
|         externalUrl: json["external_url"], | ||||
|         author: Author.fromJson(json["author"]), | ||||
|         articleId: json["article_id"], | ||||
|         momentId: json["moment_id"], | ||||
|         commentId: json["comment_id"], | ||||
|         authorId: json["author_id"], | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'], | ||||
|         fileId: json['file_id'], | ||||
|         filesize: json['filesize'], | ||||
|         filename: json['filename'], | ||||
|         mimetype: json['mimetype'], | ||||
|         type: json['type'], | ||||
|         externalUrl: json['external_url'], | ||||
|         author: Author.fromJson(json['author']), | ||||
|         articleId: json['article_id'], | ||||
|         momentId: json['moment_id'], | ||||
|         commentId: json['comment_id'], | ||||
|         authorId: json['author_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         "id": id, | ||||
|         "created_at": createdAt.toIso8601String(), | ||||
|         "updated_at": updatedAt.toIso8601String(), | ||||
|         "deleted_at": deletedAt, | ||||
|         "file_id": fileId, | ||||
|         "filesize": filesize, | ||||
|         "filename": filename, | ||||
|         "mimetype": mimetype, | ||||
|         "type": type, | ||||
|         "external_url": externalUrl, | ||||
|         "author": author.toJson(), | ||||
|         "article_id": articleId, | ||||
|         "moment_id": momentId, | ||||
|         "comment_id": commentId, | ||||
|         "author_id": authorId, | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt, | ||||
|         'file_id': fileId, | ||||
|         'filesize': filesize, | ||||
|         'filename': filename, | ||||
|         'mimetype': mimetype, | ||||
|         'type': type, | ||||
|         'external_url': externalUrl, | ||||
|         'author': author.toJson(), | ||||
|         'article_id': articleId, | ||||
|         'moment_id': momentId, | ||||
|         'comment_id': commentId, | ||||
|         'author_id': authorId, | ||||
|       }; | ||||
| } | ||||
|   | ||||
| @@ -14,12 +14,12 @@ class AuthProvider extends ChangeNotifier { | ||||
|   final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); | ||||
|   final redirectUrl = Uri.parse('solian://auth'); | ||||
|  | ||||
|   static const clientId = "solian"; | ||||
|   static const clientSecret = "_F4%q2Eea3"; | ||||
|   static const clientId = 'solian'; | ||||
|   static const clientSecret = '_F4%q2Eea3'; | ||||
|  | ||||
|   static const storage = FlutterSecureStorage(); | ||||
|   static const storageKey = "identity"; | ||||
|   static const profileKey = "profiles"; | ||||
|   static const storageKey = 'identity'; | ||||
|   static const profileKey = 'profiles'; | ||||
|  | ||||
|   /// Before use this variable to make request | ||||
|   /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD** | ||||
| @@ -57,7 +57,7 @@ class AuthProvider extends ChangeNotifier { | ||||
|       password, | ||||
|       identifier: clientId, | ||||
|       secret: clientSecret, | ||||
|       scopes: ["openid"], | ||||
|       scopes: ['openid'], | ||||
|       basicAuth: false, | ||||
|     ); | ||||
|   } | ||||
| @@ -115,6 +115,6 @@ class AuthProvider extends ChangeNotifier { | ||||
|  | ||||
|   Future<dynamic> getProfiles() async { | ||||
|     const storage = FlutterSecureStorage(); | ||||
|     return jsonDecode(await storage.read(key: profileKey) ?? "{}"); | ||||
|     return jsonDecode(await storage.read(key: profileKey) ?? '{}'); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -17,9 +18,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| class ChatProvider extends ChangeNotifier { | ||||
|   bool isOpened = false; | ||||
|   bool isShown = false; | ||||
|   bool isCallShown = false; | ||||
|  | ||||
|   ChatCallInstance? call; | ||||
|   Call? ongoingCall; | ||||
|   Channel? focusChannel; | ||||
|   ChatCallInstance? currentCall; | ||||
|  | ||||
|   Future<WebSocketChannel?> connect(AuthProvider auth) async { | ||||
|     if (auth.client == null) await auth.loadClient(); | ||||
| @@ -43,17 +46,51 @@ class ChatProvider extends ChangeNotifier { | ||||
|     return channel; | ||||
|   } | ||||
|  | ||||
|   bool handleCall(Call call, Channel channel, | ||||
|       {Function? onUpdate, Function? onDispose}) { | ||||
|     if (this.call != null) return false; | ||||
|   Future<Channel> fetchChannel(String alias) async { | ||||
|     final Client client = Client(); | ||||
|  | ||||
|     this.call = ChatCallInstance( | ||||
|     var uri = getRequestUri('messaging', '/api/channels/$alias'); | ||||
|     var res = await client.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)); | ||||
|       focusChannel = Channel.fromJson(result); | ||||
|       notifyListeners(); | ||||
|       return focusChannel!; | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       throw Exception(message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<Call?> fetchOngoingCall(String alias) async { | ||||
|     final Client client = Client(); | ||||
|  | ||||
|     var uri = getRequestUri('messaging', '/api/channels/$alias/calls/ongoing'); | ||||
|     var res = await client.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)); | ||||
|       ongoingCall = Call.fromJson(result); | ||||
|       notifyListeners(); | ||||
|       return ongoingCall; | ||||
|     } else if (res.statusCode != 404) { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       throw Exception(message); | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool handleCallJoin(Call call, Channel channel, | ||||
|       {Function? onUpdate, Function? onDispose}) { | ||||
|     if (currentCall != null) return false; | ||||
|  | ||||
|     currentCall = ChatCallInstance( | ||||
|       onUpdate: () { | ||||
|         notifyListeners(); | ||||
|         if (onUpdate != null) onUpdate(); | ||||
|       }, | ||||
|       onDispose: () { | ||||
|         this.call = null; | ||||
|         currentCall = null; | ||||
|         notifyListeners(); | ||||
|         if (onDispose != null) onDispose(); | ||||
|       }, | ||||
| @@ -64,8 +101,13 @@ class ChatProvider extends ChangeNotifier { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   void setShown(bool state) { | ||||
|     isShown = state; | ||||
|   void setOngoingCall(Call? item) { | ||||
|     ongoingCall = item; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   void setCallShown(bool state) { | ||||
|     isCallShown = state; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
| @@ -118,8 +160,9 @@ class ChatCallInstance { | ||||
|   } | ||||
|  | ||||
|   Future<void> checkPermissions() async { | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     await Permission.camera.request(); | ||||
|     await Permission.microphone.request(); | ||||
| @@ -133,7 +176,7 @@ class ChatCallInstance { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     if (!await auth.isAuthorized()) { | ||||
|       onDispose(); | ||||
|       throw Exception("unauthorized"); | ||||
|       throw Exception('unauthorized'); | ||||
|     } | ||||
|  | ||||
|     var uri = getRequestUri( | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class NotifyProvider extends ChangeNotifier { | ||||
|     const androidSettings = AndroidInitializationSettings('app_icon'); | ||||
|     const darwinSettings = DarwinInitializationSettings( | ||||
|       notificationCategories: [ | ||||
|         DarwinNotificationCategory("general"), | ||||
|         DarwinNotificationCategory('general'), | ||||
|       ], | ||||
|     ); | ||||
|     const linuxSettings = | ||||
| @@ -46,8 +46,9 @@ class NotifyProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   Future<void> requestPermissions() async { | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) | ||||
|     if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) { | ||||
|       return; | ||||
|     } | ||||
|     await Permission.notification.request(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -97,7 +97,7 @@ class NameCard extends StatelessWidget { | ||||
|   Future<Widget> renderAvatar(BuildContext context) async { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     final profiles = await auth.getProfiles(); | ||||
|     return AccountAvatar(source: profiles["picture"], direct: true); | ||||
|     return AccountAvatar(source: profiles['picture'], direct: true); | ||||
|   } | ||||
|  | ||||
|   Future<Column> renderLabel(BuildContext context) async { | ||||
| @@ -107,13 +107,13 @@ class NameCard extends StatelessWidget { | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Text( | ||||
|           profiles["nick"], | ||||
|           profiles['nick'], | ||||
|           style: const TextStyle( | ||||
|             fontSize: 20, | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|         Text(profiles["email"]) | ||||
|         Text(profiles['email']) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -157,8 +157,9 @@ 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) | ||||
|     if (relation.status == 0 && relation.relatedId != _selfId) { | ||||
|       return DismissDirection.startToEnd; | ||||
|     } | ||||
|     return DismissDirection.horizontal; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,41 +0,0 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'package:webview_flutter/webview_flutter.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| class AuthorizationScreen extends StatelessWidget { | ||||
|   final Uri authorizationUrl; | ||||
|  | ||||
|   const AuthorizationScreen(this.authorizationUrl, {super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(AppLocalizations.of(context)!.signIn), | ||||
|       ), | ||||
|       body: Stack(children: [ | ||||
|         WebViewWidget( | ||||
|           controller: WebViewController() | ||||
|             ..setJavaScriptMode(JavaScriptMode.unrestricted) | ||||
|             ..setBackgroundColor(Colors.white) | ||||
|             ..setNavigationDelegate(NavigationDelegate( | ||||
|               onNavigationRequest: (NavigationRequest request) { | ||||
|                 if (request.url.startsWith('solian')) { | ||||
|                   Navigator.of(context).pop(request.url); | ||||
|                   WebViewCookieManager().clearCookies(); | ||||
|                   return NavigationDecision.prevent; | ||||
|                 } else if (request.url.contains("sign-up")) { | ||||
|                   launchUrl(Uri.parse(request.url)); | ||||
|                   return NavigationDecision.prevent; | ||||
|                 } | ||||
|                 return NavigationDecision.navigate; | ||||
|               }, | ||||
|             )) | ||||
|             ..loadRequest(authorizationUrl) | ||||
|             ..clearCache(), | ||||
|         ), | ||||
|       ]), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -24,14 +24,14 @@ class _ChatCallState extends State<ChatCall> { | ||||
|  | ||||
|   late ChatProvider _chat; | ||||
|  | ||||
|   ChatCallInstance get _call => _chat.call!; | ||||
|   ChatCallInstance get _call => _chat.currentCall!; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       _chat.setShown(true); | ||||
|       _chat.setCallShown(true); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -40,13 +40,13 @@ class _ChatCallState extends State<ChatCall> { | ||||
|     _chat = context.watch<ChatProvider>(); | ||||
|     if (!_isHandled) { | ||||
|       _isHandled = true; | ||||
|       if (_chat.handleCall(widget.call, widget.call.channel)) { | ||||
|         _chat.call?.init(); | ||||
|       if (_chat.handleCallJoin(widget.call, widget.call.channel)) { | ||||
|         _chat.currentCall?.init(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Widget content; | ||||
|     if (_chat.call == null) { | ||||
|     if (_chat.currentCall == null) { | ||||
|       content = const Center( | ||||
|         child: CircularProgressIndicator(), | ||||
|       ); | ||||
| @@ -136,7 +136,7 @@ class _ChatCallState extends State<ChatCall> { | ||||
|  | ||||
|   @override | ||||
|   void deactivate() { | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false)); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setCallShown(false)); | ||||
|     super.deactivate(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -42,7 +42,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> { | ||||
|         ? getRequestUri('messaging', '/api/channels') | ||||
|         : getRequestUri('messaging', '/api/channels/${widget.editing!.id}'); | ||||
|  | ||||
|     final req = Request(widget.editing == null ? "POST" : "PUT", uri); | ||||
|     final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); | ||||
|     req.headers['Content-Type'] = 'application/json'; | ||||
|     req.body = jsonEncode(<String, dynamic>{ | ||||
|       'alias': _aliasController.value.text.toLowerCase(), | ||||
|   | ||||
| @@ -4,69 +4,52 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:solian/models/call.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/models/message.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/chat.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/chat/channel_action.dart'; | ||||
| import 'package:solian/widgets/chat/maintainer.dart'; | ||||
| import 'package:solian/widgets/chat/message.dart'; | ||||
| import 'package:solian/widgets/chat/message_action.dart'; | ||||
| import 'package:solian/widgets/chat/message_editor.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
|  | ||||
| class ChatScreen extends StatefulWidget { | ||||
| class ChatScreen extends StatelessWidget { | ||||
|   final String alias; | ||||
|  | ||||
|   const ChatScreen({super.key, required this.alias}); | ||||
|  | ||||
|   @override | ||||
|   State<ChatScreen> createState() => _ChatScreenState(); | ||||
|   Widget build(BuildContext context) { | ||||
|     return IndentWrapper( | ||||
|       title: AppLocalizations.of(context)!.post, | ||||
|       noSafeArea: true, | ||||
|       hideDrawer: true, | ||||
|       child: ChatScreenWidget( | ||||
|         alias: alias, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ChatScreenState extends State<ChatScreen> { | ||||
|   Call? _ongoingCall; | ||||
|   Channel? _channelMeta; | ||||
| class ChatScreenWidget extends StatefulWidget { | ||||
|   final String alias; | ||||
|  | ||||
|   const ChatScreenWidget({super.key, required this.alias}); | ||||
|  | ||||
|   @override | ||||
|   State<ChatScreenWidget> createState() => _ChatScreenWidgetState(); | ||||
| } | ||||
|  | ||||
| class _ChatScreenWidgetState extends State<ChatScreenWidget> { | ||||
|   bool _isReady = false; | ||||
|  | ||||
|   final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); | ||||
|  | ||||
|   final http.Client _client = http.Client(); | ||||
|  | ||||
|   Future<Channel> fetchMetadata() async { | ||||
|     var uri = getRequestUri('messaging', '/api/channels/${widget.alias}'); | ||||
|     var res = await _client.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = jsonDecode(utf8.decode(res.bodyBytes)); | ||||
|       setState(() => _channelMeta = Channel.fromJson(result)); | ||||
|       return _channelMeta!; | ||||
|     } else { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<Call?> fetchCall() async { | ||||
|     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)); | ||||
|       setState(() => _ongoingCall = Call.fromJson(result)); | ||||
|       return _ongoingCall; | ||||
|     } else if (res.statusCode != 404) { | ||||
|       var message = utf8.decode(res.bodyBytes); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|   late final ChatProvider _chat; | ||||
|  | ||||
|   Future<void> fetchMessages(int pageKey, BuildContext context) async { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
| @@ -142,11 +125,6 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     Future.delayed(Duration.zero, () { | ||||
|       fetchMetadata(); | ||||
|       fetchCall(); | ||||
|     }); | ||||
|  | ||||
|     _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); | ||||
|  | ||||
|     super.initState(); | ||||
| @@ -180,6 +158,11 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (!_isReady) { | ||||
|       _isReady = true; | ||||
|       _chat = context.watch<ChatProvider>(); | ||||
|     } | ||||
|  | ||||
|     final callBanner = MaterialBanner( | ||||
|       padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), | ||||
|       leading: const Icon(Icons.call_received), | ||||
| @@ -192,7 +175,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|           onPressed: () { | ||||
|             router.pushNamed( | ||||
|               'chat.channel.call', | ||||
|               extra: _ongoingCall, | ||||
|               extra: _chat.ongoingCall, | ||||
|               pathParameters: {'channel': widget.alias}, | ||||
|             ); | ||||
|           }, | ||||
| @@ -200,69 +183,52 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       hideDrawer: true, | ||||
|       title: _channelMeta?.name ?? 'Loading...', | ||||
|       appBarActions: _channelMeta != null | ||||
|           ? [ | ||||
|               ChannelCallAction( | ||||
|                 call: _ongoingCall, | ||||
|                 channel: _channelMeta!, | ||||
|                 onUpdate: () => fetchMetadata(), | ||||
|               ), | ||||
|               ChannelManageAction( | ||||
|                 channel: _channelMeta!, | ||||
|                 onUpdate: () => fetchMetadata(), | ||||
|               ), | ||||
|             ] | ||||
|           : [], | ||||
|       child: FutureBuilder( | ||||
|         future: fetchMetadata(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (!snapshot.hasData || snapshot.data == null) { | ||||
|             return const Center(child: CircularProgressIndicator()); | ||||
|           } | ||||
|     return FutureBuilder( | ||||
|       future: _chat.fetchChannel(widget.alias), | ||||
|       builder: (context, snapshot) { | ||||
|         if (!snapshot.hasData || snapshot.data == null) { | ||||
|           return const Center(child: CircularProgressIndicator()); | ||||
|         } | ||||
|  | ||||
|           return ChatMaintainer( | ||||
|             channel: snapshot.data!, | ||||
|             child: Stack( | ||||
|               children: [ | ||||
|                 Column( | ||||
|                   children: [ | ||||
|                     Expanded( | ||||
|                       child: PagedListView<int, Message>( | ||||
|                         reverse: true, | ||||
|                         pagingController: _pagingController, | ||||
|                         builderDelegate: PagedChildBuilderDelegate<Message>( | ||||
|                           animateTransitions: true, | ||||
|                           transitionDuration: 500.ms, | ||||
|                           itemBuilder: chatHistoryBuilder, | ||||
|                           noItemsFoundIndicatorBuilder: (_) => Container(), | ||||
|                         ), | ||||
|         return ChatMaintainer( | ||||
|           channel: snapshot.data!, | ||||
|           child: Stack( | ||||
|             children: [ | ||||
|               Column( | ||||
|                 children: [ | ||||
|                   Expanded( | ||||
|                     child: PagedListView<int, Message>( | ||||
|                       reverse: true, | ||||
|                       pagingController: _pagingController, | ||||
|                       builderDelegate: PagedChildBuilderDelegate<Message>( | ||||
|                         animateTransitions: true, | ||||
|                         transitionDuration: 500.ms, | ||||
|                         itemBuilder: chatHistoryBuilder, | ||||
|                         noItemsFoundIndicatorBuilder: (_) => Container(), | ||||
|                       ), | ||||
|                     ), | ||||
|                     ChatMessageEditor( | ||||
|                       channel: widget.alias, | ||||
|                       editing: _editingItem, | ||||
|                       replying: _replyingItem, | ||||
|                       onReset: () => setState(() { | ||||
|                         _editingItem = null; | ||||
|                         _replyingItem = null; | ||||
|                       }), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 _ongoingCall != null ? callBanner.animate().slideY() : Container(), | ||||
|               ], | ||||
|             ), | ||||
|             onInsertMessage: (message) => addMessage(message), | ||||
|             onUpdateMessage: (message) => updateMessage(message), | ||||
|             onDeleteMessage: (message) => deleteMessage(message), | ||||
|             onCallStarted: (call) => setState(() => _ongoingCall = call), | ||||
|             onCallEnded: () => setState(() => _ongoingCall = null), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|                   ), | ||||
|                   ChatMessageEditor( | ||||
|                     channel: widget.alias, | ||||
|                     editing: _editingItem, | ||||
|                     replying: _replyingItem, | ||||
|                     onReset: () => setState(() { | ||||
|                       _editingItem = null; | ||||
|                       _replyingItem = null; | ||||
|                     }), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               _chat.ongoingCall != null ? callBanner.animate().slideY() : Container(), | ||||
|             ], | ||||
|           ), | ||||
|           onInsertMessage: (message) => addMessage(message), | ||||
|           onUpdateMessage: (message) => updateMessage(message), | ||||
|           onDeleteMessage: (message) => deleteMessage(message), | ||||
|           onCallStarted: (call) => _chat.setOngoingCall(call), | ||||
|           onCallEnded: () => _chat.setOngoingCall(null), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,10 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/screens/chat/chat.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/chat/chat_new.dart'; | ||||
| import 'package:solian/widgets/empty.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| @@ -21,6 +23,68 @@ class ChatIndexScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _ChatIndexScreenState extends State<ChatIndexScreen> { | ||||
|   Channel? _selectedChannel; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final screenWidth = MediaQuery.of(context).size.width; | ||||
|     final isLargeScreen = screenWidth >= 600; | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       title: AppLocalizations.of(context)!.chat, | ||||
|       appBarActions: const [NotificationButton()], | ||||
|       fixedAppBarColor: isLargeScreen, | ||||
|       child: isLargeScreen | ||||
|           ? Row( | ||||
|               children: [ | ||||
|                 Flexible( | ||||
|                   flex: 2, | ||||
|                   child: ChatIndexScreenWidget( | ||||
|                     onSelect: (item) { | ||||
|                       setState(() => _selectedChannel = item); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const VerticalDivider(thickness: 0.3, width: 0.3), | ||||
|                 Flexible( | ||||
|                   flex: 4, | ||||
|                   child: _selectedChannel == null | ||||
|                       ? const SelectionEmptyWidget() | ||||
|                       : ChatScreenWidget( | ||||
|                           key: Key('c${_selectedChannel!.id}'), | ||||
|                           alias: _selectedChannel!.alias, | ||||
|                         ), | ||||
|                 ), | ||||
|               ], | ||||
|             ) | ||||
|           : ChatIndexScreenWidget( | ||||
|               onSelect: (item) async { | ||||
|                 final result = await router.pushNamed( | ||||
|                   'chat.channel', | ||||
|                   pathParameters: { | ||||
|                     'channel': item.alias, | ||||
|                   }, | ||||
|                 ); | ||||
|                 switch (result) { | ||||
|                   case 'refresh': | ||||
|                   // fetchChannels(); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ChatIndexScreenWidget extends StatefulWidget { | ||||
|   final Function(Channel item) onSelect; | ||||
|  | ||||
|   const ChatIndexScreenWidget({super.key, required this.onSelect}); | ||||
|  | ||||
|   @override | ||||
|   State<ChatIndexScreenWidget> createState() => _ChatIndexScreenWidgetState(); | ||||
| } | ||||
|  | ||||
| class _ChatIndexScreenWidgetState extends State<ChatIndexScreenWidget> { | ||||
|   List<Channel> _channels = List.empty(); | ||||
|  | ||||
|   Future<void> fetchChannels() async { | ||||
| @@ -61,9 +125,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       title: AppLocalizations.of(context)!.chat, | ||||
|       appBarActions: const [NotificationButton()], | ||||
|     return Scaffold( | ||||
|       floatingActionButton: FutureBuilder( | ||||
|         future: auth.isAuthorized(), | ||||
|         builder: (context, snapshot) { | ||||
| @@ -78,43 +140,33 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> { | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|       child: FutureBuilder( | ||||
|           future: auth.isAuthorized(), | ||||
|           builder: (context, snapshot) { | ||||
|             if (!snapshot.hasData || !snapshot.data!) { | ||||
|               return const SignInRequiredScreen(); | ||||
|             } | ||||
|       body: FutureBuilder( | ||||
|         future: auth.isAuthorized(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (!snapshot.hasData || !snapshot.data!) { | ||||
|             return const SignInRequiredScreen(); | ||||
|           } | ||||
|  | ||||
|             return RefreshIndicator( | ||||
|               onRefresh: () => fetchChannels(), | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _channels.length, | ||||
|                 itemBuilder: (context, index) { | ||||
|                   final element = _channels[index]; | ||||
|                   return ListTile( | ||||
|                     leading: const CircleAvatar( | ||||
|                       backgroundColor: Colors.indigo, | ||||
|                       child: Icon(Icons.tag, color: Colors.white), | ||||
|                     ), | ||||
|                     title: Text(element.name), | ||||
|                     subtitle: Text(element.description), | ||||
|                     onTap: () async { | ||||
|                       final result = await router.pushNamed( | ||||
|                         'chat.channel', | ||||
|                         pathParameters: { | ||||
|                           'channel': element.alias, | ||||
|                         }, | ||||
|                       ); | ||||
|                       switch (result) { | ||||
|                         case 'refresh': | ||||
|                           fetchChannels(); | ||||
|                       } | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ); | ||||
|           }), | ||||
|           return RefreshIndicator( | ||||
|             onRefresh: () => fetchChannels(), | ||||
|             child: ListView.builder( | ||||
|               itemCount: _channels.length, | ||||
|               itemBuilder: (context, index) { | ||||
|                 final element = _channels[index]; | ||||
|                 return ListTile( | ||||
|                   leading: const CircleAvatar( | ||||
|                     backgroundColor: Colors.indigo, | ||||
|                     child: Icon(Icons.tag, color: Colors.white), | ||||
|                   ), | ||||
|                   title: Text(element.name), | ||||
|                   subtitle: Text(element.description), | ||||
|                   onTap: () => widget.onSelect(element), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,10 +6,12 @@ import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/screens/posts/screen.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
| import 'package:solian/widgets/empty.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:solian/widgets/notification_notifier.dart'; | ||||
| import 'package:solian/widgets/posts/item.dart'; | ||||
| @@ -22,8 +24,68 @@ class ExploreScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _ExploreScreenState extends State<ExploreScreen> { | ||||
|   final PagingController<int, Post> _pagingController = | ||||
|       PagingController(firstPageKey: 0); | ||||
|   Post? _selectedPost; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final screenWidth = MediaQuery.of(context).size.width; | ||||
|     final isLargeScreen = screenWidth >= 600; | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       noSafeArea: true, | ||||
|       fixedAppBarColor: isLargeScreen, | ||||
|       appBarActions: const [NotificationButton()], | ||||
|       title: AppLocalizations.of(context)!.explore, | ||||
|       child: isLargeScreen | ||||
|           ? Row( | ||||
|               children: [ | ||||
|                 Flexible( | ||||
|                   flex: 2, | ||||
|                   child: ExploreScreenWidget( | ||||
|                     onSelect: (item) { | ||||
|                       setState(() => _selectedPost = item); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const VerticalDivider(thickness: 0.3, width: 0.3), | ||||
|                 Flexible( | ||||
|                   flex: 4, | ||||
|                   child: _selectedPost == null | ||||
|                       ? const SelectionEmptyWidget() | ||||
|                       : PostScreenWidget( | ||||
|                           key: Key('p${_selectedPost!.id}'), | ||||
|                           dataset: _selectedPost!.dataset, | ||||
|                           alias: _selectedPost!.alias, | ||||
|                         ), | ||||
|                 ), | ||||
|               ], | ||||
|             ) | ||||
|           : ExploreScreenWidget( | ||||
|               onSelect: (item) { | ||||
|                 router.pushNamed( | ||||
|                   'posts.screen', | ||||
|                   pathParameters: { | ||||
|                     'alias': item.alias, | ||||
|                     'dataset': item.dataset, | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ExploreScreenWidget extends StatefulWidget { | ||||
|   final Function(Post item) onSelect; | ||||
|  | ||||
|   const ExploreScreenWidget({super.key, required this.onSelect}); | ||||
|  | ||||
|   @override | ||||
|   State<ExploreScreenWidget> createState() => _ExploreScreenWidgetState(); | ||||
| } | ||||
|  | ||||
| class _ExploreScreenWidgetState extends State<ExploreScreenWidget> { | ||||
|   final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0); | ||||
|  | ||||
|   final http.Client _client = http.Client(); | ||||
|  | ||||
| @@ -31,15 +93,12 @@ 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); | ||||
| @@ -63,8 +122,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       noSafeArea: true, | ||||
|     return Scaffold( | ||||
|       floatingActionButton: FutureBuilder( | ||||
|         future: auth.isAuthorized(), | ||||
|         builder: (context, snapshot) { | ||||
| @@ -72,7 +130,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|             return FloatingActionButton( | ||||
|               child: const Icon(Icons.edit), | ||||
|               onPressed: () async { | ||||
|                 final did = await router.pushNamed("posts.moments.editor"); | ||||
|                 final did = await router.pushNamed('posts.moments.editor'); | ||||
|                 if (did == true) _pagingController.refresh(); | ||||
|               }, | ||||
|             ); | ||||
| @@ -81,9 +139,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|       appBarActions: const [NotificationButton()], | ||||
|       title: AppLocalizations.of(context)!.explore, | ||||
|       child: RefreshIndicator( | ||||
|       body: RefreshIndicator( | ||||
|         onRefresh: () => Future.sync( | ||||
|           () => _pagingController.refresh(), | ||||
|         ), | ||||
| @@ -93,15 +149,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|             itemBuilder: (context, item, index) => PostItem( | ||||
|               item: item, | ||||
|               onUpdate: () => _pagingController.refresh(), | ||||
|               onTap: () { | ||||
|                 router.pushNamed( | ||||
|                   'posts.screen', | ||||
|                   pathParameters: { | ||||
|                     'alias': item.alias, | ||||
|                     'dataset': '${item.modelType}s', | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|               onTap: () => widget.onSelect(item), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|   | ||||
| @@ -97,7 +97,7 @@ class NotificationItem extends StatelessWidget { | ||||
|               padding: const EdgeInsets.only( | ||||
|                   left: 16, right: 16, top: 34, bottom: 12), | ||||
|               child: Text( | ||||
|                 "Links", | ||||
|                 'Links', | ||||
|                 style: Theme.of(context).textTheme.headlineSmall, | ||||
|               ), | ||||
|             ), | ||||
| @@ -151,7 +151,7 @@ class NotificationItem extends StatelessWidget { | ||||
|                       text: item.subject, | ||||
|                       style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ), | ||||
|                     const TextSpan(text: " is marked as read") | ||||
|                     const TextSpan(text: ' is marked as read') | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|   | ||||
| @@ -65,7 +65,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | ||||
|         ? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments') | ||||
|         : getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}'); | ||||
|  | ||||
|     final req = Request(widget.editing == null ? "POST" : "PUT", uri); | ||||
|     final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); | ||||
|     req.headers['Content-Type'] = 'application/json'; | ||||
|     req.body = jsonEncode(<String, dynamic>{ | ||||
|       'alias': _alias, | ||||
| @@ -140,12 +140,12 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> { | ||||
|               if (snapshot.hasData) { | ||||
|                 var userinfo = snapshot.data; | ||||
|                 return ListTile( | ||||
|                   title: Text(userinfo["nick"]), | ||||
|                   title: Text(userinfo['nick']), | ||||
|                   subtitle: Text( | ||||
|                     AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                   ), | ||||
|                   leading: AccountAvatar( | ||||
|                     source: userinfo["picture"], | ||||
|                     source: userinfo['picture'], | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                 ); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | ||||
|         ? getRequestUri('interactive', '/api/p/moments') | ||||
|         : getRequestUri('interactive', '/api/p/moments/${widget.editing!.id}'); | ||||
|  | ||||
|     final req = Request(widget.editing == null ? "POST" : "PUT", uri); | ||||
|     final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); | ||||
|     req.headers['Content-Type'] = 'application/json'; | ||||
|     req.body = jsonEncode(<String, dynamic>{ | ||||
|       'alias': _alias, | ||||
| @@ -130,12 +130,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> { | ||||
|               if (snapshot.hasData) { | ||||
|                 var userinfo = snapshot.data; | ||||
|                 return ListTile( | ||||
|                   title: Text(userinfo["nick"]), | ||||
|                   title: Text(userinfo['nick']), | ||||
|                   subtitle: Text( | ||||
|                     AppLocalizations.of(context)!.postIdentityNotify, | ||||
|                   ), | ||||
|                   leading: AccountAvatar( | ||||
|                     source: userinfo["picture"], | ||||
|                     source: userinfo['picture'], | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                 ); | ||||
|   | ||||
| @@ -11,25 +11,43 @@ import 'package:solian/widgets/posts/comment_list.dart'; | ||||
| import 'package:solian/widgets/posts/item.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| class PostScreen extends StatefulWidget { | ||||
| class PostScreen extends StatelessWidget { | ||||
|   final String dataset; | ||||
|   final String alias; | ||||
|  | ||||
|   const PostScreen({super.key, required this.alias, required this.dataset}); | ||||
|  | ||||
|   @override | ||||
|   State<PostScreen> createState() => _PostScreenState(); | ||||
|   Widget build(BuildContext context) { | ||||
|     return IndentWrapper( | ||||
|       title: AppLocalizations.of(context)!.post, | ||||
|       noSafeArea: true, | ||||
|       hideDrawer: true, | ||||
|       child: PostScreenWidget( | ||||
|         dataset: dataset, | ||||
|         alias: alias, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostScreenState extends State<PostScreen> { | ||||
| class PostScreenWidget extends StatefulWidget { | ||||
|   final String dataset; | ||||
|   final String alias; | ||||
|  | ||||
|   const PostScreenWidget({super.key, required this.dataset, required this.alias}); | ||||
|  | ||||
|   @override | ||||
|   State<PostScreenWidget> createState() => _PostScreenWidgetState(); | ||||
| } | ||||
|  | ||||
| class _PostScreenWidgetState extends State<PostScreenWidget> { | ||||
|   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); | ||||
| @@ -42,43 +60,38 @@ class _PostScreenState extends State<PostScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return IndentWrapper( | ||||
|       noSafeArea: true, | ||||
|       hideDrawer: true, | ||||
|       title: AppLocalizations.of(context)!.post, | ||||
|       child: FutureBuilder( | ||||
|         future: fetchPost(context), | ||||
|         builder: (context, snapshot) { | ||||
|           if (snapshot.hasData && snapshot.data != null) { | ||||
|             return CustomScrollView( | ||||
|               slivers: [ | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: PostItem( | ||||
|                     item: snapshot.data!, | ||||
|                     brief: false, | ||||
|                     ripple: false, | ||||
|                   ), | ||||
|     return FutureBuilder( | ||||
|       future: fetchPost(context), | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.hasData && snapshot.data != null) { | ||||
|           return CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverToBoxAdapter( | ||||
|                 child: PostItem( | ||||
|                   item: snapshot.data!, | ||||
|                   brief: false, | ||||
|                   ripple: false, | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: CommentListHeader( | ||||
|                     related: snapshot.data!, | ||||
|                     paging: _commentPagingController, | ||||
|                   ), | ||||
|                 ), | ||||
|                 CommentList( | ||||
|               ), | ||||
|               SliverToBoxAdapter( | ||||
|                 child: CommentListHeader( | ||||
|                   related: snapshot.data!, | ||||
|                   dataset: widget.dataset, | ||||
|                   paging: _commentPagingController, | ||||
|                 ), | ||||
|               ], | ||||
|             ); | ||||
|           } else { | ||||
|             return const Center( | ||||
|               child: CircularProgressIndicator(), | ||||
|             ); | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|               ), | ||||
|               CommentList( | ||||
|                 related: snapshot.data!, | ||||
|                 dataset: widget.dataset, | ||||
|                 paging: _commentPagingController, | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         } else { | ||||
|           return const Center( | ||||
|             child: CircularProgressIndicator(), | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class CallOverlay extends StatelessWidget { | ||||
|  | ||||
|     final chat = context.watch<ChatProvider>(); | ||||
|  | ||||
|     if (chat.isShown || chat.call == null) { | ||||
|     if (chat.isCallShown || chat.currentCall == null) { | ||||
|       return Container(); | ||||
|     } | ||||
|  | ||||
| @@ -54,8 +54,8 @@ class CallOverlay extends StatelessWidget { | ||||
|       onTap: () { | ||||
|         router.pushNamed( | ||||
|           'chat.channel.call', | ||||
|           extra: chat.call!.info, | ||||
|           pathParameters: {'channel': chat.call!.channel.alias}, | ||||
|           extra: chat.currentCall!.info, | ||||
|           pathParameters: {'channel': chat.currentCall!.channel.alias}, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -73,9 +73,9 @@ class _ControlsWidgetState extends State<ControlsWidget> { | ||||
|     if (await context.showDisconnectDialog() != true) return; | ||||
|  | ||||
|     final chat = context.read<ChatProvider>(); | ||||
|     if (chat.call != null) { | ||||
|       chat.call!.deactivate(); | ||||
|       chat.call!.dispose(); | ||||
|     if (chat.currentCall != null) { | ||||
|       chat.currentCall!.deactivate(); | ||||
|       chat.currentCall!.dispose(); | ||||
|       router.pop(); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -55,23 +55,19 @@ 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!); | ||||
|   | ||||
| @@ -56,7 +56,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|         ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') | ||||
|         : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); | ||||
|  | ||||
|     final req = Request(widget.editing == null ? "POST" : "PUT", uri); | ||||
|     final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); | ||||
|     req.headers['Content-Type'] = 'application/json'; | ||||
|     req.body = jsonEncode(<String, dynamic>{ | ||||
|       'content': _textController.value.text, | ||||
| @@ -163,7 +163,6 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|                   focusNode: _focusNode, | ||||
|                   controller: _textController, | ||||
|                   maxLines: null, | ||||
|                   autofocus: true, | ||||
|                   autocorrect: true, | ||||
|                   keyboardType: TextInputType.text, | ||||
|                   decoration: InputDecoration.collapsed( | ||||
|   | ||||
| @@ -22,7 +22,11 @@ class LayoutWrapper extends StatelessWidget { | ||||
|     final content = child ?? Container(); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(title), actions: appBarActions), | ||||
|       appBar: AppBar( | ||||
|         title: Text(title), | ||||
|         actions: appBarActions, | ||||
|         centerTitle: false, | ||||
|       ), | ||||
|       floatingActionButton: floatingActionButton, | ||||
|       drawer: const SolianNavigationDrawer(), | ||||
|       body: noSafeArea ? content : SafeArea(child: content), | ||||
|   | ||||
							
								
								
									
										23
									
								
								lib/widgets/empty.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/widgets/empty.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
|  | ||||
| class SelectionEmptyWidget extends StatelessWidget { | ||||
|   const SelectionEmptyWidget({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Center( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           Image.asset('assets/logo.png', width: 64, height: 64), | ||||
|           const SizedBox(height: 2), | ||||
|           Text( | ||||
|             AppLocalizations.of(context)!.appName, | ||||
|             style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -9,8 +9,8 @@ extension SolianCommonExtensions on BuildContext { | ||||
|       return message | ||||
|           .split(' ') | ||||
|           .map((element) => | ||||
|               "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") | ||||
|           .join(" "); | ||||
|               '${element[0].toUpperCase()}${element.substring(1).toLowerCase()}') | ||||
|           .join(' '); | ||||
|     } | ||||
|  | ||||
|     return showDialog<void>( | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:solian/widgets/navigation_drawer.dart'; | ||||
|  | ||||
| class IndentWrapper extends LayoutWrapper { | ||||
|   final bool hideDrawer; | ||||
|   final bool fixedAppBarColor; | ||||
|  | ||||
|   const IndentWrapper({ | ||||
|     super.key, | ||||
| @@ -13,6 +14,7 @@ class IndentWrapper extends LayoutWrapper { | ||||
|     super.floatingActionButton, | ||||
|     super.appBarActions, | ||||
|     this.hideDrawer = false, | ||||
|     this.fixedAppBarColor = false, | ||||
|     super.noSafeArea = false, | ||||
|   }) : super(); | ||||
|  | ||||
| @@ -30,6 +32,8 @@ class IndentWrapper extends LayoutWrapper { | ||||
|             : null, | ||||
|         title: Text(title), | ||||
|         actions: appBarActions, | ||||
|         centerTitle: false, | ||||
|         elevation: fixedAppBarColor ? 4 : null, | ||||
|       ), | ||||
|       floatingActionButton: floatingActionButton, | ||||
|       drawer: const SolianNavigationDrawer(), | ||||
|   | ||||
| @@ -39,21 +39,21 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> { | ||||
|           icon: const Icon(Icons.explore), | ||||
|           label: Text(AppLocalizations.of(context)!.explore), | ||||
|         ), | ||||
|         "explore", | ||||
|         'explore', | ||||
|       ), | ||||
|       ( | ||||
|         NavigationDrawerDestination( | ||||
|           icon: const Icon(Icons.send), | ||||
|           label: Text(AppLocalizations.of(context)!.chat), | ||||
|         ), | ||||
|         "chat", | ||||
|         'chat', | ||||
|       ), | ||||
|       ( | ||||
|         NavigationDrawerDestination( | ||||
|           icon: const Icon(Icons.account_circle), | ||||
|           label: Text(AppLocalizations.of(context)!.account), | ||||
|         ), | ||||
|         "account", | ||||
|         'account', | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
| @@ -69,7 +69,7 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> { | ||||
|           child: Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.start, | ||||
|             children: [ | ||||
|               Image.asset("assets/logo.png", width: 26, height: 26), | ||||
|               Image.asset('assets/logo.png', width: 26, height: 26), | ||||
|               const SizedBox(width: 10), | ||||
|               Text( | ||||
|                 AppLocalizations.of(context)!.appName, | ||||
|   | ||||
| @@ -85,7 +85,7 @@ class _NotificationButtonState extends State<NotificationButton> { | ||||
|       child: IconButton( | ||||
|         icon: const Icon(Icons.notifications), | ||||
|         onPressed: () { | ||||
|           router.pushNamed("notification"); | ||||
|           router.pushNamed('notification'); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -112,7 +112,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|     var res = await auth.client!.send(req); | ||||
|     if (res.statusCode == 200) { | ||||
|       var result = Attachment.fromJson( | ||||
|         jsonDecode(utf8.decode(await res.stream.toBytes()))["info"], | ||||
|         jsonDecode(utf8.decode(await res.stream.toBytes()))['info'], | ||||
|       ); | ||||
|       setState(() => _attachments.add(result)); | ||||
|       widget.onUpdate(_attachments); | ||||
| @@ -252,7 +252,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> { | ||||
|                             style: Theme.of(context).textTheme.titleMedium, | ||||
|                           ), | ||||
|                           Text( | ||||
|                             "${getFileType(element)} · ${formatBytes(element.filesize)}", | ||||
|                             '${getFileType(element)} · ${formatBytes(element.filesize)}', | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|   | ||||
| @@ -121,7 +121,7 @@ class CommentListHeader extends StatelessWidget { | ||||
|                 return TextButton( | ||||
|                   onPressed: () async { | ||||
|                     final did = await router.pushNamed( | ||||
|                       "posts.comments.editor", | ||||
|                       'posts.comments.editor', | ||||
|                       extra: CommentPostArguments(related: related), | ||||
|                     ); | ||||
|                     if (did == true) paging.refresh(); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class ArticleContent extends StatelessWidget { | ||||
|                 }, | ||||
|                 imageBuilder: (url, _, __) { | ||||
|                   Uri uri; | ||||
|                   if (url.toString().startsWith("/api/attachments")) { | ||||
|                   if (url.toString().startsWith('/api/attachments')) { | ||||
|                     uri = getRequestUri('interactive', url.toString()); | ||||
|                   } else { | ||||
|                     uri = url; | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class _AttachmentItemState extends State<AttachmentItem> { | ||||
|  | ||||
|   late final _videoPlayer = Player( | ||||
|     configuration: PlayerConfiguration( | ||||
|       title: "Attachment #${getTag()}", | ||||
|       title: 'Attachment #${getTag()}', | ||||
|       logLevel: MPVLogLevel.error, | ||||
|     ), | ||||
|   ); | ||||
|   | ||||
| @@ -13,8 +13,8 @@ import 'package:timeago/timeago.dart' as timeago; | ||||
|  | ||||
| class PostItem extends StatefulWidget { | ||||
|   final Post item; | ||||
|   final bool? brief; | ||||
|   final bool? ripple; | ||||
|   final bool brief; | ||||
|   final bool ripple; | ||||
|   final Function? onUpdate; | ||||
|   final Function? onDelete; | ||||
|   final Function? onTap; | ||||
| @@ -22,8 +22,8 @@ class PostItem extends StatefulWidget { | ||||
|   const PostItem({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.brief, | ||||
|     this.ripple, | ||||
|     this.brief = true, | ||||
|     this.ripple = true, | ||||
|     this.onUpdate, | ||||
|     this.onDelete, | ||||
|     this.onTap, | ||||
| @@ -79,9 +79,9 @@ class _PostItemState extends State<PostItem> { | ||||
|   Widget renderContent() { | ||||
|     switch (widget.item.modelType) { | ||||
|       case 'article': | ||||
|         return ArticleContent(item: widget.item, brief: widget.brief ?? true); | ||||
|         return ArticleContent(item: widget.item, brief: widget.brief); | ||||
|       default: | ||||
|         return MomentContent(item: widget.item, brief: widget.brief ?? true); | ||||
|         return MomentContent(item: widget.item, brief: widget.brief); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -163,7 +163,7 @@ class _PostItemState extends State<PostItem> { | ||||
|  | ||||
|     Widget content; | ||||
|  | ||||
|     if (widget.brief ?? true) { | ||||
|     if (widget.brief) { | ||||
|       content = Padding( | ||||
|         padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), | ||||
|         child: Column( | ||||
| @@ -199,7 +199,7 @@ class _PostItemState extends State<PostItem> { | ||||
|       content = Column( | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(left: 12, right: 12, top: 16), | ||||
|             padding: const EdgeInsets.only(left: 20, right: 20, top: 16), | ||||
|             child: Row( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
| @@ -230,17 +230,17 @@ class _PostItemState extends State<PostItem> { | ||||
|             child: Divider(thickness: 0.3), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), | ||||
|             child: renderContent(), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|             child: renderAttachments(), | ||||
|           ), | ||||
|           ClipRRect( | ||||
|             borderRadius: BorderRadius.circular(8), | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), | ||||
|               child: renderReactions(), | ||||
|             ), | ||||
|           ), | ||||
| @@ -248,9 +248,7 @@ class _PostItemState extends State<PostItem> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final ripple = widget.ripple ?? true; | ||||
|  | ||||
|     if (ripple) { | ||||
|     if (widget.ripple) { | ||||
|       return InkWell( | ||||
|         child: content, | ||||
|         onTap: () { | ||||
|   | ||||
| @@ -134,8 +134,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> { | ||||
|                 child: ListTile( | ||||
|                   title: Text(info.value.icon), | ||||
|                   subtitle: Text( | ||||
|                     ":${info.key}:", | ||||
|                     style: const TextStyle(fontFamily: "monospace"), | ||||
|                     ':${info.key}:', | ||||
|                     style: const TextStyle(fontFamily: 'monospace'), | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user