diff --git a/lib/main.dart b/lib/main.dart index e018e0d..4b9d2ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/post.dart'; import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/friend.dart'; +import 'package:solian/providers/status.dart'; import 'package:solian/router.dart'; import 'package:solian/theme.dart'; import 'package:solian/translations.dart'; @@ -79,6 +80,7 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => ChatProvider()); Get.lazyPut(() => AccountProvider()); + Get.lazyPut(() => StatusController()); Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => ChatCallProvider()); diff --git a/lib/models/account_status.dart b/lib/models/account_status.dart new file mode 100644 index 0000000..ef6cebd --- /dev/null +++ b/lib/models/account_status.dart @@ -0,0 +1,27 @@ +class AccountStatus { + bool isDisturbable; + bool isOnline; + DateTime? lastSeenAt; + dynamic status; + + AccountStatus({ + required this.isDisturbable, + required this.isOnline, + required this.lastSeenAt, + required this.status, + }); + + factory AccountStatus.fromJson(Map json) => AccountStatus( + isDisturbable: json['is_disturbable'], + isOnline: json['is_online'], + lastSeenAt: json['last_seen_at'] != null ? DateTime.parse(json['last_seen_at']) : null, + status: json['status'], + ); + + Map toJson() => { + 'is_disturbable': isDisturbable, + 'is_online': isOnline, + 'last_seen_at': lastSeenAt?.toIso8601String(), + 'status': status, + }; +} diff --git a/lib/models/personal_page.dart b/lib/models/personal_page.dart deleted file mode 100644 index 4f72834..0000000 --- a/lib/models/personal_page.dart +++ /dev/null @@ -1,49 +0,0 @@ -class PersonalPage { - int id; - DateTime createdAt; - DateTime updatedAt; - DateTime? deletedAt; - String content; - String script; - String style; - Map? links; - int accountId; - - PersonalPage({ - required this.id, - required this.createdAt, - required this.updatedAt, - required this.deletedAt, - required this.content, - required this.script, - required this.style, - required this.links, - required this.accountId, - }); - - factory PersonalPage.fromJson(Map json) => PersonalPage( - id: json['id'], - createdAt: DateTime.parse(json['created_at']), - updatedAt: DateTime.parse(json['updated_at']), - deletedAt: json['deleted_at'] != null - ? DateTime.parse(json['deleted_at']) - : null, - content: json['content'], - script: json['script'], - style: json['style'], - links: json['links'], - accountId: json['account_id'], - ); - - Map toJson() => { - 'id': id, - 'created_at': createdAt.toIso8601String(), - 'updated_at': updatedAt.toIso8601String(), - 'deleted_at': deletedAt?.toIso8601String(), - 'content': content, - 'script': script, - 'style': style, - 'links': links, - 'account_id': accountId, - }; -} diff --git a/lib/providers/status.dart b/lib/providers/status.dart new file mode 100644 index 0000000..da17e57 --- /dev/null +++ b/lib/providers/status.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/models/account_status.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/services.dart'; + +class StatusController extends GetConnect { + static Map presetStatuses = { + 'online': ( + const Icon(Icons.circle, color: Colors.green), + 'accountStatusOnline'.tr, + null, + ), + 'silent': ( + const Icon(Icons.do_not_disturb_on, color: Colors.red), + 'accountStatusSilent'.tr, + 'accountStatusSilentDesc'.tr, + ), + 'invisible': ( + const Icon(Icons.circle, color: Colors.grey), + 'accountStatusInvisible'.tr, + 'accountStatusInvisibleDesc'.tr, + ), + }; + + @override + void onInit() { + final AuthProvider auth = Get.find(); + + httpClient.baseUrl = ServiceFinder.services['passport']; + httpClient.addAuthenticator(auth.requestAuthenticator); + } + + Future getCurrentStatus() => get('/api/users/me/status'); + + Future getSomeoneStatus(String name) => + get('/api/users/$name/status'); + + static (Widget, String) determineStatus(AccountStatus status, + {double size = 14}) { + Widget icon; + String? text = status.status?.label; + + if (status.isDisturbable && status.isOnline) { + icon = Icon(Icons.circle, color: Colors.green, size: size); + text ??= 'accountStatusOnline'.tr; + } else if (!status.isDisturbable && status.isOnline) { + icon = Icon(Icons.do_not_disturb_on, color: Colors.red, size: size); + text ??= 'accountStatusSilent'.tr; + } else { + icon = Icon(Icons.circle, color: Colors.grey, size: size); + text ??= 'accountStatusOffline'.tr; + } + return (icon, text); + } +} diff --git a/lib/screens/account.dart b/lib/screens/account.dart index f2d06ad..01a466e 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/models/account.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/status.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signup.dart'; @@ -132,6 +133,7 @@ class AccountHeading extends StatelessWidget { name: prof.body['name'], nick: prof.body['nick'], desc: prof.body['description'], + status: Get.find().getCurrentStatus(), badges: prof.body['badges'] ?.map((e) => AccountBadge.fromJson(e)) .toList() diff --git a/lib/translations.dart b/lib/translations.dart index e8ec8c4..913cc72 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -216,6 +216,15 @@ class SolianMessages extends Translations { 'pushNotifyRegisterDone': 'Push notifications has been activated.', 'pushNotifyRegisterFailed': 'Unable to active push notification... @reason', + 'accountChangeStatus': 'Change Status', + 'accountCustomStatus': 'Set Custom Status', + 'accountClearStatus': 'Clear Status', + 'accountStatusOnline': 'Online', + 'accountStatusSilent': 'Do not Disturb', + 'accountStatusSilentDesc': 'The notification will stop popping up', + 'accountStatusInvisible': 'Invisible', + 'accountStatusInvisibleDesc': 'Will show as offline, but all features still remain normal', + 'accountStatusOffline': 'Offline', }, 'zh_CN': { 'hide': '隐藏', @@ -415,6 +424,15 @@ class SolianMessages extends Translations { '激活推送通知便可以让你在应用程序完全关闭的时候仍然获取到我们最新的通知。在 iOS/macOS 设备上我们使用 Apple 官方的推送服务;其他设备则通过 Google Firebase 提供推送通知。要注册推送通知设备,您可能需要连接到 Google 的服务器(在中国大陆不可用)并在您的设备上安装 Google Framework。即使您关闭此对话框,下次启动应用程序时仍会自动执行此注册。', 'pushNotifyRegisterDone': '推送通知已成功激活', 'pushNotifyRegisterFailed': '推送通知激活失败…… @reason', + 'accountChangeStatus': '变更状态', + 'accountCustomStatus': '自定义状态', + 'accountClearStatus': '清除状态', + 'accountStatusOnline': '在线', + 'accountStatusSilent': '请勿打扰', + 'accountStatusSilentDesc': '将会暂停所有通知推送', + 'accountStatusInvisible': '隐身', + 'accountStatusInvisibleDesc': '将会在他人界面显示离线,但不影响功能使用', + 'accountStatusOffline': '离线', } }; } diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index efe03c5..f61b0a1 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/models/account.dart'; +import 'package:solian/models/account_status.dart'; import 'package:solian/platform.dart'; +import 'package:solian/providers/status.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_badge.dart'; +import 'package:solian/widgets/account/account_status_action.dart'; class AccountHeadingWidget extends StatelessWidget { final dynamic avatar; @@ -13,6 +16,9 @@ class AccountHeadingWidget extends StatelessWidget { final String? desc; final List? badges; + final Future? status; + final Function? onEditStatus; + const AccountHeadingWidget({ super.key, this.avatar, @@ -21,8 +27,18 @@ class AccountHeadingWidget extends StatelessWidget { required this.nick, required this.desc, required this.badges, + this.status, + this.onEditStatus, }); + void showStatusAction(BuildContext context, bool hasStatus) { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => AccountStatusAction(hasStatus: hasStatus), + ); + } + @override Widget build(BuildContext context) { return Material( @@ -55,23 +71,59 @@ class AccountHeadingWidget extends StatelessWidget { ), ], ), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, + Column( children: [ - Text( - nick, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.bold, - ), - ).paddingOnly(right: 4), - Text( - '@$name', - style: const TextStyle( - fontSize: 15, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + nick, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ).paddingOnly(right: 4), + Text( + '@$name', + style: const TextStyle( + fontSize: 15, + ), + ), + ], ), + if (status != null) + SizedBox( + width: double.infinity, + child: FutureBuilder( + future: status, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data!.body == null) { + return Text('loading'.tr); + } + + final info = StatusController.determineStatus( + AccountStatus.fromJson(snapshot.data!.body), + ); + + return GestureDetector( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(info.$2), + info.$1.paddingSymmetric(horizontal: 6), + ], + ), + onTap: () { + showStatusAction( + context, + snapshot.data!.body['status'] != null, + ); + }, + ); + }, + ), + ), ], ).paddingOnly(left: 116, top: 6), const SizedBox(height: 4), diff --git a/lib/widgets/account/account_profile_popup.dart b/lib/widgets/account/account_profile_popup.dart index 6da8135..20bfec6 100644 --- a/lib/widgets/account/account_profile_popup.dart +++ b/lib/widgets/account/account_profile_popup.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/account.dart'; +import 'package:solian/providers/status.dart'; import 'package:solian/services.dart'; import 'package:solian/widgets/account/account_heading.dart'; @@ -60,6 +61,7 @@ class _AccountProfilePopupState extends State { nick: _userinfo!.nick, desc: _userinfo!.description, badges: _userinfo!.badges, + status: Get.find().getSomeoneStatus(_userinfo!.name), ).paddingOnly(top: 16), ], ), diff --git a/lib/widgets/account/account_status_action.dart b/lib/widgets/account/account_status_action.dart new file mode 100644 index 0000000..327ee27 --- /dev/null +++ b/lib/widgets/account/account_status_action.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/providers/status.dart'; + +class AccountStatusAction extends StatelessWidget { + final bool hasStatus; + + const AccountStatusAction({super.key, this.hasStatus = false}); + + @override + Widget build(BuildContext context) { + return SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'accountChangeStatus'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Expanded( + child: ListView( + children: [ + SizedBox( + height: 48, + child: ListView( + scrollDirection: Axis.horizontal, + children: StatusController.presetStatuses.entries + .map( + (x) => ActionChip( + avatar: x.value.$1, + label: Text(x.value.$2), + tooltip: x.value.$3, + onPressed: () {}, + ).paddingOnly(right: 6), + ) + .toList(), + ).paddingSymmetric(horizontal: 18), + ), + const Divider(thickness: 0.3, height: 0.3) + .paddingSymmetric(vertical: 16), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: hasStatus + ? const Icon(Icons.edit) + : const Icon(Icons.add), + title: Text('accountCustomStatus'.tr), + onTap: () {}, + ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Icons.clear), + title: Text('accountClearStatus'.tr), + onTap: () {}, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/posts/post_quick_action.dart b/lib/widgets/posts/post_quick_action.dart index 35279c5..ca65baf 100644 --- a/lib/widgets/posts/post_quick_action.dart +++ b/lib/widgets/posts/post_quick_action.dart @@ -31,7 +31,6 @@ class _PostQuickActionState extends State { void showReactMenu() { showModalBottomSheet( useRootNavigator: true, - isScrollControlled: true, context: context, builder: (context) => PostReactionPopup( item: widget.item, diff --git a/lib/widgets/posts/post_reaction.dart b/lib/widgets/posts/post_reaction.dart index 5f29db5..202e125 100644 --- a/lib/widgets/posts/post_reaction.dart +++ b/lib/widgets/posts/post_reaction.dart @@ -12,7 +12,6 @@ class PostReactionPopup extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: MediaQuery.of(context).size.height * 0.85, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [