From b39c8c770e6a789b1796194ac4c83173206ccf74 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 2 May 2024 00:49:38 +0800 Subject: [PATCH] :sparkles: Basic large screen support --- lib/models/account.dart | 48 +++--- lib/models/author.dart | 48 +++--- lib/models/call.dart | 36 ++--- lib/models/channel.dart | 84 +++++----- lib/models/friendship.dart | 40 ++--- lib/models/message.dart | 90 +++++------ lib/models/notification.dart | 58 +++---- lib/models/packet.dart | 12 +- lib/models/pagination.dart | 6 +- lib/models/post.dart | 128 +++++++-------- lib/providers/auth.dart | 12 +- lib/providers/chat.dart | 65 ++++++-- lib/providers/notify.dart | 5 +- lib/screens/account.dart | 6 +- lib/screens/account/friend.dart | 3 +- lib/screens/auth.dart | 41 ----- lib/screens/chat/call.dart | 12 +- lib/screens/chat/channel/editor.dart | 2 +- lib/screens/chat/chat.dart | 180 +++++++++------------- lib/screens/chat/index.dart | 130 +++++++++++----- lib/screens/explore.dart | 94 ++++++++--- lib/screens/notification.dart | 4 +- lib/screens/posts/comment_editor.dart | 6 +- lib/screens/posts/moment_editor.dart | 6 +- lib/screens/posts/screen.dart | 93 ++++++----- lib/widgets/chat/call/call_overlay.dart | 6 +- lib/widgets/chat/call/controls.dart | 6 +- lib/widgets/chat/maintainer.dart | 12 +- lib/widgets/chat/message_editor.dart | 3 +- lib/widgets/common_wrapper.dart | 6 +- lib/widgets/empty.dart | 23 +++ lib/widgets/exts.dart | 4 +- lib/widgets/indent_wrapper.dart | 4 + lib/widgets/navigation_drawer.dart | 8 +- lib/widgets/notification_notifier.dart | 2 +- lib/widgets/posts/attachment_editor.dart | 4 +- lib/widgets/posts/comment_list.dart | 2 +- lib/widgets/posts/content/article.dart | 2 +- lib/widgets/posts/content/attachment.dart | 2 +- lib/widgets/posts/item.dart | 26 ++-- lib/widgets/posts/reaction_action.dart | 4 +- 41 files changed, 716 insertions(+), 607 deletions(-) delete mode 100755 lib/screens/auth.dart create mode 100644 lib/widgets/empty.dart diff --git a/lib/models/account.dart b/lib/models/account.dart index 312492e..e3321ee 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -28,32 +28,32 @@ class Account { }); factory Account.fromJson(Map 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 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, }; } diff --git a/lib/models/author.dart b/lib/models/author.dart index 4301757..f903094 100755 --- a/lib/models/author.dart +++ b/lib/models/author.dart @@ -28,32 +28,32 @@ class Author { }); factory Author.fromJson(Map 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 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, }; } diff --git a/lib/models/call.dart b/lib/models/call.dart index 74f85d8..8dfe638 100644 --- a/lib/models/call.dart +++ b/lib/models/call.dart @@ -25,28 +25,28 @@ class Call { }); factory Call.fromJson(Map 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 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(), }; } diff --git a/lib/models/channel.dart b/lib/models/channel.dart index 4b295d4..a783551 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -32,35 +32,35 @@ class Channel { }); factory Channel.fromJson(Map 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 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 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 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, }; } diff --git a/lib/models/friendship.dart b/lib/models/friendship.dart index 442e638..6737619 100644 --- a/lib/models/friendship.dart +++ b/lib/models/friendship.dart @@ -26,29 +26,29 @@ class Friendship { }); factory Friendship.fromJson(Map 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 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) { diff --git a/lib/models/message.dart b/lib/models/message.dart index af99f7c..17a95ea 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -36,42 +36,42 @@ class Message { }); factory Message.fromJson(Map 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.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 toJson() => { - "id": id, - "created_at": createdAt.toIso8601String(), - "updated_at": updatedAt.toIso8601String(), - "deleted_at": deletedAt, - "content": content, - "metadata": metadata, - "type": type, - "attachments": List.from( + 'id': id, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'deleted_at': deletedAt, + 'content': content, + 'metadata': metadata, + 'type': type, + 'attachments': List.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 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 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, }; } diff --git a/lib/models/notification.dart b/lib/models/notification.dart index 0f61674..189f8f1 100755 --- a/lib/models/notification.dart +++ b/lib/models/notification.dart @@ -28,37 +28,37 @@ class Notification { }); factory Notification.fromJson(Map 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.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.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 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.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 json) => Link( - label: json["label"], - url: json["url"], + label: json['label'], + url: json['url'], ); Map toJson() => { - "label": label, - "url": url, + 'label': label, + 'url': url, }; } diff --git a/lib/models/packet.dart b/lib/models/packet.dart index 3e701b6..c633a4c 100644 --- a/lib/models/packet.dart +++ b/lib/models/packet.dart @@ -10,14 +10,14 @@ class NetworkPackage { }); factory NetworkPackage.fromJson(Map json) => NetworkPackage( - method: json["w"], - message: json["m"], - payload: json["p"], + method: json['w'], + message: json['m'], + payload: json['p'], ); Map toJson() => { - "w": method, - "m": message, - "p": payload, + 'w': method, + 'm': message, + 'p': payload, }; } diff --git a/lib/models/pagination.dart b/lib/models/pagination.dart index 8330eb3..b1107d7 100755 --- a/lib/models/pagination.dart +++ b/lib/models/pagination.dart @@ -8,10 +8,10 @@ class PaginationResult { }); factory PaginationResult.fromJson(Map json) => - PaginationResult(count: json["count"], data: json["data"]); + PaginationResult(count: json['count'], data: json['data']); Map toJson() => { - "count": count, - "data": data, + 'count': count, + 'data': data, }; } diff --git a/lib/models/post.dart b/lib/models/post.dart index 457057c..58d4f81 100755 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -18,6 +18,8 @@ class Post { List? attachments; Map? reactionList; + String get dataset => '${modelType}s'; + Post({ required this.id, required this.createdAt, @@ -38,46 +40,46 @@ class Post { }); factory Post.fromJson(Map 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.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 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.from(attachments!.map((x) => x.toJson())), - "reaction_list": reactionList, + 'reaction_list': reactionList, }; } @@ -117,38 +119,38 @@ class Attachment { }); factory Attachment.fromJson(Map 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 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, }; } diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 0ed08a7..1d87ff4 100755 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -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 getProfiles() async { const storage = FlutterSecureStorage(); - return jsonDecode(await storage.read(key: profileKey) ?? "{}"); + return jsonDecode(await storage.read(key: profileKey) ?? '{}'); } } diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index 12e999f..1ef50fa 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -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 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 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 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 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(); if (!await auth.isAuthorized()) { onDispose(); - throw Exception("unauthorized"); + throw Exception('unauthorized'); } var uri = getRequestUri( diff --git a/lib/providers/notify.dart b/lib/providers/notify.dart index 382ee0f..a16292d 100644 --- a/lib/providers/notify.dart +++ b/lib/providers/notify.dart @@ -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 requestPermissions() async { - if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) + if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) { return; + } await Permission.notification.request(); } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 0670f26..d41737d 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -97,7 +97,7 @@ class NameCard extends StatelessWidget { Future renderAvatar(BuildContext context) async { final auth = context.read(); final profiles = await auth.getProfiles(); - return AccountAvatar(source: profiles["picture"], direct: true); + return AccountAvatar(source: profiles['picture'], direct: true); } Future 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']) ], ); } diff --git a/lib/screens/account/friend.dart b/lib/screens/account/friend.dart index 21cbf70..b2390af 100644 --- a/lib/screens/account/friend.dart +++ b/lib/screens/account/friend.dart @@ -157,8 +157,9 @@ class _FriendScreenState extends State { 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; } diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart deleted file mode 100755 index cf739e3..0000000 --- a/lib/screens/auth.dart +++ /dev/null @@ -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(), - ), - ]), - ); - } -} diff --git a/lib/screens/chat/call.dart b/lib/screens/chat/call.dart index bb167a9..3b37dc8 100644 --- a/lib/screens/chat/call.dart +++ b/lib/screens/chat/call.dart @@ -24,14 +24,14 @@ class _ChatCallState extends State { 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 { _chat = context.watch(); 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 { @override void deactivate() { - WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false)); + WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setCallShown(false)); super.deactivate(); } } diff --git a/lib/screens/chat/channel/editor.dart b/lib/screens/chat/channel/editor.dart index b3c6f9f..a1379c0 100644 --- a/lib/screens/chat/channel/editor.dart +++ b/lib/screens/chat/channel/editor.dart @@ -42,7 +42,7 @@ class _ChannelEditorScreenState extends State { ? 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({ 'alias': _aliasController.value.text.toLowerCase(), diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 41936f9..507aa3a 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -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 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 { - Call? _ongoingCall; - Channel? _channelMeta; +class ChatScreenWidget extends StatefulWidget { + final String alias; + + const ChatScreenWidget({super.key, required this.alias}); + + @override + State createState() => _ChatScreenWidgetState(); +} + +class _ChatScreenWidgetState extends State { + bool _isReady = false; final PagingController _pagingController = PagingController(firstPageKey: 0); - final http.Client _client = http.Client(); - - Future 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 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 fetchMessages(int pageKey, BuildContext context) async { final auth = context.read(); @@ -142,11 +125,6 @@ class _ChatScreenState extends State { @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 { ); } + if (!_isReady) { + _isReady = true; + _chat = context.watch(); + } + 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 { onPressed: () { router.pushNamed( 'chat.channel.call', - extra: _ongoingCall, + extra: _chat.ongoingCall, pathParameters: {'channel': widget.alias}, ); }, @@ -200,69 +183,52 @@ class _ChatScreenState extends State { ], ); - 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( - reverse: true, - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - animateTransitions: true, - transitionDuration: 500.ms, - itemBuilder: chatHistoryBuilder, - noItemsFoundIndicatorBuilder: (_) => Container(), - ), + return ChatMaintainer( + channel: snapshot.data!, + child: Stack( + children: [ + Column( + children: [ + Expanded( + child: PagedListView( + reverse: true, + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + 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), + ); + }, ); } } diff --git a/lib/screens/chat/index.dart b/lib/screens/chat/index.dart index c6b507f..cf43cd6 100644 --- a/lib/screens/chat/index.dart +++ b/lib/screens/chat/index.dart @@ -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 { + 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 createState() => _ChatIndexScreenWidgetState(); +} + +class _ChatIndexScreenWidgetState extends State { List _channels = List.empty(); Future fetchChannels() async { @@ -61,9 +125,7 @@ class _ChatIndexScreenState extends State { Widget build(BuildContext context) { final auth = context.read(); - 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 { } }, ), - 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), + ); + }, + ), + ); + }, + ), ); } } diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index daaefe5..b0c0cbd 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -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 { - final PagingController _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 createState() => _ExploreScreenWidgetState(); +} + +class _ExploreScreenWidgetState extends State { + final PagingController _pagingController = PagingController(firstPageKey: 0); final http.Client _client = http.Client(); @@ -31,15 +93,12 @@ class _ExploreScreenState extends State { 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 { Widget build(BuildContext context) { final auth = context.read(); - return IndentWrapper( - noSafeArea: true, + return Scaffold( floatingActionButton: FutureBuilder( future: auth.isAuthorized(), builder: (context, snapshot) { @@ -72,7 +130,7 @@ class _ExploreScreenState extends State { 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 { } }, ), - 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 { 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), ), ), ), diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index 415f866..11004a9 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -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') ], ), ), diff --git a/lib/screens/posts/comment_editor.dart b/lib/screens/posts/comment_editor.dart index 7c5d3ca..9133149 100644 --- a/lib/screens/posts/comment_editor.dart +++ b/lib/screens/posts/comment_editor.dart @@ -65,7 +65,7 @@ class _CommentEditorScreenState extends State { ? 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({ 'alias': _alias, @@ -140,12 +140,12 @@ class _CommentEditorScreenState extends State { 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, ), ); diff --git a/lib/screens/posts/moment_editor.dart b/lib/screens/posts/moment_editor.dart index 9aee018..b29820c 100644 --- a/lib/screens/posts/moment_editor.dart +++ b/lib/screens/posts/moment_editor.dart @@ -55,7 +55,7 @@ class _MomentEditorScreenState extends State { ? 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({ 'alias': _alias, @@ -130,12 +130,12 @@ class _MomentEditorScreenState extends State { 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, ), ); diff --git a/lib/screens/posts/screen.dart b/lib/screens/posts/screen.dart index 0bd1b75..07afa97 100644 --- a/lib/screens/posts/screen.dart +++ b/lib/screens/posts/screen.dart @@ -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 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 { +class PostScreenWidget extends StatefulWidget { + final String dataset; + final String alias; + + const PostScreenWidget({super.key, required this.dataset, required this.alias}); + + @override + State createState() => _PostScreenWidgetState(); +} + +class _PostScreenWidgetState extends State { final _client = http.Client(); - final PagingController _commentPagingController = - PagingController(firstPageKey: 0); + final PagingController _commentPagingController = PagingController(firstPageKey: 0); Future 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 { @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(), + ); + } + }, ); } diff --git a/lib/widgets/chat/call/call_overlay.dart b/lib/widgets/chat/call/call_overlay.dart index e5e637f..1a23689 100644 --- a/lib/widgets/chat/call/call_overlay.dart +++ b/lib/widgets/chat/call/call_overlay.dart @@ -14,7 +14,7 @@ class CallOverlay extends StatelessWidget { final chat = context.watch(); - 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}, ); }, ); diff --git a/lib/widgets/chat/call/controls.dart b/lib/widgets/chat/call/controls.dart index 017c91b..089578e 100644 --- a/lib/widgets/chat/call/controls.dart +++ b/lib/widgets/chat/call/controls.dart @@ -73,9 +73,9 @@ class _ControlsWidgetState extends State { if (await context.showDisconnectDialog() != true) return; final chat = context.read(); - if (chat.call != null) { - chat.call!.deactivate(); - chat.call!.dispose(); + if (chat.currentCall != null) { + chat.currentCall!.deactivate(); + chat.currentCall!.dispose(); router.pop(); } } diff --git a/lib/widgets/chat/maintainer.dart b/lib/widgets/chat/maintainer.dart index d2c59bd..23f0e95 100644 --- a/lib/widgets/chat/maintainer.dart +++ b/lib/widgets/chat/maintainer.dart @@ -55,23 +55,19 @@ class _ChatMaintainerState extends State { 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!); diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index dcfebbe..42b0bf0 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -56,7 +56,7 @@ class _ChatMessageEditorState extends State { ? 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({ 'content': _textController.value.text, @@ -163,7 +163,6 @@ class _ChatMessageEditorState extends State { focusNode: _focusNode, controller: _textController, maxLines: null, - autofocus: true, autocorrect: true, keyboardType: TextInputType.text, decoration: InputDecoration.collapsed( diff --git a/lib/widgets/common_wrapper.dart b/lib/widgets/common_wrapper.dart index 9821810..b42ac0c 100644 --- a/lib/widgets/common_wrapper.dart +++ b/lib/widgets/common_wrapper.dart @@ -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), diff --git a/lib/widgets/empty.dart b/lib/widgets/empty.dart new file mode 100644 index 0000000..668648e --- /dev/null +++ b/lib/widgets/empty.dart @@ -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), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/exts.dart b/lib/widgets/exts.dart index 984e236..182fd33 100644 --- a/lib/widgets/exts.dart +++ b/lib/widgets/exts.dart @@ -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( diff --git a/lib/widgets/indent_wrapper.dart b/lib/widgets/indent_wrapper.dart index 2939f58..bd2de73 100644 --- a/lib/widgets/indent_wrapper.dart +++ b/lib/widgets/indent_wrapper.dart @@ -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(), diff --git a/lib/widgets/navigation_drawer.dart b/lib/widgets/navigation_drawer.dart index 5d174d6..79d7a0e 100644 --- a/lib/widgets/navigation_drawer.dart +++ b/lib/widgets/navigation_drawer.dart @@ -39,21 +39,21 @@ class _SolianNavigationDrawerState extends State { 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 { 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, diff --git a/lib/widgets/notification_notifier.dart b/lib/widgets/notification_notifier.dart index fcfd5b1..0fab09f 100644 --- a/lib/widgets/notification_notifier.dart +++ b/lib/widgets/notification_notifier.dart @@ -85,7 +85,7 @@ class _NotificationButtonState extends State { child: IconButton( icon: const Icon(Icons.notifications), onPressed: () { - router.pushNamed("notification"); + router.pushNamed('notification'); }, ), ); diff --git a/lib/widgets/posts/attachment_editor.dart b/lib/widgets/posts/attachment_editor.dart index 1446cc6..9929892 100755 --- a/lib/widgets/posts/attachment_editor.dart +++ b/lib/widgets/posts/attachment_editor.dart @@ -112,7 +112,7 @@ class _AttachmentEditorState extends State { 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 { style: Theme.of(context).textTheme.titleMedium, ), Text( - "${getFileType(element)} · ${formatBytes(element.filesize)}", + '${getFileType(element)} · ${formatBytes(element.filesize)}', ), ], ), diff --git a/lib/widgets/posts/comment_list.dart b/lib/widgets/posts/comment_list.dart index c31d11a..2c5cb53 100644 --- a/lib/widgets/posts/comment_list.dart +++ b/lib/widgets/posts/comment_list.dart @@ -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(); diff --git a/lib/widgets/posts/content/article.dart b/lib/widgets/posts/content/article.dart index 5af5070..c30d824 100644 --- a/lib/widgets/posts/content/article.dart +++ b/lib/widgets/posts/content/article.dart @@ -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; diff --git a/lib/widgets/posts/content/attachment.dart b/lib/widgets/posts/content/attachment.dart index 28343ae..57ff723 100644 --- a/lib/widgets/posts/content/attachment.dart +++ b/lib/widgets/posts/content/attachment.dart @@ -30,7 +30,7 @@ class _AttachmentItemState extends State { late final _videoPlayer = Player( configuration: PlayerConfiguration( - title: "Attachment #${getTag()}", + title: 'Attachment #${getTag()}', logLevel: MPVLogLevel.error, ), ); diff --git a/lib/widgets/posts/item.dart b/lib/widgets/posts/item.dart index fb4219d..76066b6 100644 --- a/lib/widgets/posts/item.dart +++ b/lib/widgets/posts/item.dart @@ -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 { 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 { 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 { 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 { 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 { ); } - final ripple = widget.ripple ?? true; - - if (ripple) { + if (widget.ripple) { return InkWell( child: content, onTap: () { diff --git a/lib/widgets/posts/reaction_action.dart b/lib/widgets/posts/reaction_action.dart index 99ea870..b200735 100644 --- a/lib/widgets/posts/reaction_action.dart +++ b/lib/widgets/posts/reaction_action.dart @@ -134,8 +134,8 @@ class _ReactionActionPopupState extends State { child: ListTile( title: Text(info.value.icon), subtitle: Text( - ":${info.key}:", - style: const TextStyle(fontFamily: "monospace"), + ':${info.key}:', + style: const TextStyle(fontFamily: 'monospace'), ), ), );