From 42acffff3edccdb07995d6e91fb1bde7c8336b27 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 27 Jun 2024 01:33:03 +0800 Subject: [PATCH] :sparkles: Preset statuses --- lib/main.dart | 4 +- lib/models/account_status.dart | 62 +++++++++- lib/providers/account_status.dart | 117 ++++++++++++++++++ lib/providers/status.dart | 56 --------- lib/screens/account.dart | 24 +++- lib/screens/channel/channel_chat.dart | 5 +- lib/widgets/account/account_heading.dart | 10 +- .../account/account_profile_popup.dart | 4 +- .../account/account_status_action.dart | 115 +++++++++++------ 9 files changed, 286 insertions(+), 111 deletions(-) create mode 100644 lib/providers/account_status.dart delete mode 100644 lib/providers/status.dart diff --git a/lib/main.dart b/lib/main.dart index 4b9d2ee..aeaa694 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +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/providers/account_status.dart'; import 'package:solian/router.dart'; import 'package:solian/theme.dart'; import 'package:solian/translations.dart'; @@ -80,7 +80,7 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => ChatProvider()); Get.lazyPut(() => AccountProvider()); - Get.lazyPut(() => StatusController()); + Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => ChatCallProvider()); diff --git a/lib/models/account_status.dart b/lib/models/account_status.dart index ef6cebd..38a6747 100644 --- a/lib/models/account_status.dart +++ b/lib/models/account_status.dart @@ -2,7 +2,7 @@ class AccountStatus { bool isDisturbable; bool isOnline; DateTime? lastSeenAt; - dynamic status; + Status? status; AccountStatus({ required this.isDisturbable, @@ -15,13 +15,69 @@ class 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'], + status: json['status'] != null ? Status.fromJson(json['status']) : null, ); Map toJson() => { 'is_disturbable': isDisturbable, 'is_online': isOnline, 'last_seen_at': lastSeenAt?.toIso8601String(), - 'status': status, + 'status': status?.toJson(), + }; +} + +class Status { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String type; + String label; + int attitude; + bool isNoDisturb; + bool isInvisible; + DateTime? clearAt; + int accountId; + + Status({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.type, + required this.label, + required this.attitude, + required this.isNoDisturb, + required this.isInvisible, + required this.clearAt, + required this.accountId, + }); + + factory Status.fromJson(Map json) => Status( + 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, + type: json['type'], + label: json['label'], + attitude: json['attitude'], + isNoDisturb: json['is_no_disturb'], + isInvisible: json['is_invisible'], + clearAt: json['clear_at'] != null ? DateTime.parse(json['clear_at']) : null, + accountId: json['account_id'], + ); + + Map toJson() => { + 'id': id, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'deleted_at': deletedAt?.toIso8601String(), + 'type': type, + 'label': label, + 'attitude': attitude, + 'is_no_disturb': isNoDisturb, + 'is_invisible': isInvisible, + 'clear_at': clearAt?.toIso8601String(), + 'account_id': accountId, }; } diff --git a/lib/providers/account_status.dart b/lib/providers/account_status.dart new file mode 100644 index 0000000..bd1ebb3 --- /dev/null +++ b/lib/providers/account_status.dart @@ -0,0 +1,117 @@ +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 StatusProvider 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() async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + final client = auth.configureClient('passport'); + + return await client.get('/api/users/me/status'); + } + + Future getSomeoneStatus(String name) => + get('/api/users/$name/status'); + + Future setStatus( + String type, + String? label, + int attitude, { + bool isUpdate = false, + bool isSilent = false, + bool isInvisible = false, + DateTime? clearAt, + }) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + final client = auth.configureClient('passport'); + + final payload = { + 'type': type, + 'label': label, + 'is_no_disturb': isSilent, + 'is_invisible': isInvisible, + 'clear_at': clearAt?.toUtc().toIso8601String() + }; + + Response resp; + if (!isUpdate) { + resp = await client.post('/api/users/me/status', payload); + } else { + resp = await client.put('/api/users/me/status', payload); + } + + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } + + Future clearStatus() async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + final client = auth.configureClient('passport'); + + final resp = await client.delete('/api/users/me/status'); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } + + static (Widget, String) determineStatus(AccountStatus status, + {double size = 14}) { + Widget icon; + String? text; + + if (!presetStatuses.keys.contains(status.status?.type)) { + 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/providers/status.dart b/lib/providers/status.dart deleted file mode 100644 index da17e57..0000000 --- a/lib/providers/status.dart +++ /dev/null @@ -1,56 +0,0 @@ -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 01a466e..1c85cb2 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -2,7 +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/providers/account_status.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signup.dart'; @@ -112,9 +112,22 @@ class _AccountScreenState extends State { } } -class AccountHeading extends StatelessWidget { +class AccountHeading extends StatefulWidget { const AccountHeading({super.key}); + @override + State createState() => _AccountHeadingState(); +} + +class _AccountHeadingState extends State { + late Future _status; + + @override + void initState() { + _status = Get.find().getCurrentStatus(); + super.initState(); + } + @override Widget build(BuildContext context) { final AuthProvider provider = Get.find(); @@ -133,11 +146,16 @@ class AccountHeading extends StatelessWidget { name: prof.body['name'], nick: prof.body['nick'], desc: prof.body['description'], - status: Get.find().getCurrentStatus(), + status: _status, badges: prof.body['badges'] ?.map((e) => AccountBadge.fromJson(e)) .toList() .cast(), + onEditStatus: () { + setState(() { + _status = Get.find().getCurrentStatus(); + }); + }, ); }, ); diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 3e795ea..d756a79 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -362,10 +362,7 @@ class _ChannelChatScreenState extends State { } return SliverToBoxAdapter( - child: const LinearProgressIndicator() - .animate() - .slideY() - .paddingOnly(bottom: 4), + child: const LinearProgressIndicator().animate().slideY(), ); }), ], diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index f61b0a1..509dc04 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -3,7 +3,7 @@ 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/providers/account_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'; @@ -32,11 +32,15 @@ class AccountHeadingWidget extends StatelessWidget { }); void showStatusAction(BuildContext context, bool hasStatus) { + if (onEditStatus == null) return; + showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) => AccountStatusAction(hasStatus: hasStatus), - ); + ).then((val) { + if(val == true) onEditStatus!(); + }); } @override @@ -102,7 +106,7 @@ class AccountHeadingWidget extends StatelessWidget { return Text('loading'.tr); } - final info = StatusController.determineStatus( + final info = StatusProvider.determineStatus( AccountStatus.fromJson(snapshot.data!.body), ); diff --git a/lib/widgets/account/account_profile_popup.dart b/lib/widgets/account/account_profile_popup.dart index 20bfec6..f3f5270 100644 --- a/lib/widgets/account/account_profile_popup.dart +++ b/lib/widgets/account/account_profile_popup.dart @@ -2,7 +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/providers/account_status.dart'; import 'package:solian/services.dart'; import 'package:solian/widgets/account/account_heading.dart'; @@ -61,7 +61,7 @@ class _AccountProfilePopupState extends State { nick: _userinfo!.nick, desc: _userinfo!.description, badges: _userinfo!.badges, - status: Get.find().getSomeoneStatus(_userinfo!.name), + 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 index 327ee27..2d6ff50 100644 --- a/lib/widgets/account/account_status_action.dart +++ b/lib/widgets/account/account_status_action.dart @@ -1,14 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; -import 'package:solian/providers/status.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/providers/account_status.dart'; -class AccountStatusAction extends StatelessWidget { +class AccountStatusAction extends StatefulWidget { final bool hasStatus; const AccountStatusAction({super.key, this.hasStatus = false}); + @override + State createState() => _AccountStatusActionState(); +} + +class _AccountStatusActionState extends State { + bool _isBusy = false; + @override Widget build(BuildContext context) { + final StatusProvider controller = Get.find(); + return SizedBox( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -17,44 +28,72 @@ class AccountStatusAction extends StatelessWidget { 'accountChangeStatus'.tr, style: Theme.of(context).textTheme.headlineSmall, ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), - Expanded( + if (_isBusy) + const LinearProgressIndicator() + .paddingOnly(bottom: 14) + .animate() + .slideY(curve: Curves.fastEaseInToSlowEaseOut), + SizedBox( + height: 48, 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: () {}, - ), - ], - ), + scrollDirection: Axis.horizontal, + children: StatusProvider.presetStatuses.entries + .map( + (x) => ActionChip( + avatar: x.value.$1, + label: Text(x.value.$2), + tooltip: x.value.$3, + onPressed: _isBusy + ? null + : () async { + setState(() => _isBusy = true); + try { + await controller.setStatus( + x.key, + x.value.$2, + 0, + isInvisible: x.key == 'invisible', + isSilent: x.key == 'silent', + ); + Navigator.of(context).pop(true); + } catch (e) { + context.showErrorDialog(e); + } + setState(() => _isBusy = false); + }, + ).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: widget.hasStatus + ? const Icon(Icons.edit) + : const Icon(Icons.add), + title: Text('accountCustomStatus'.tr), + onTap: _isBusy ? null : () {}, + ), + if (widget.hasStatus) + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Icons.clear), + title: Text('accountClearStatus'.tr), + onTap: _isBusy + ? null + : () async { + setState(() => _isBusy = true); + try { + await controller.clearStatus(); + Navigator.of(context).pop(true); + } catch (e) { + context.showErrorDialog(e); + } + setState(() => _isBusy = false); + }, + ), ], ), );