From c1e89a2ee62cabd8ee38f9b879bc092d1c569489 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 26 Mar 2025 22:43:27 +0800 Subject: [PATCH] :sparkles: Punishments --- assets/translations/en-US.json | 18 +- assets/translations/zh-CN.json | 18 +- lib/providers/navigation.dart | 14 + lib/router.dart | 8 +- lib/screens/account.dart | 255 +++++----- lib/screens/account/punishments.dart | 180 +++++++ .../{account_settings.dart => settings.dart} | 0 lib/screens/friend.dart | 71 ++- lib/types/account.dart | 21 + lib/types/account.freezed.dart | 457 ++++++++++++++++++ lib/types/account.g.dart | 40 ++ 11 files changed, 920 insertions(+), 162 deletions(-) create mode 100644 lib/screens/account/punishments.dart rename lib/screens/account/{account_settings.dart => settings.dart} (100%) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 09fb20b..c158e8a 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -917,5 +917,21 @@ "accountProgramAlreadyJoined": "Joined", "accountProgramLeft": "Left Program.", "leave": "Leave", - "attachmentFailedToLoadMedia": "Unable to load media file, please try again later. If this error occurs repeatedly, the source file may not exist or the network connection may be abnormal." + "attachmentFailedToLoadMedia": "Unable to load media file, please try again later. If this error occurs repeatedly, the source file may not exist or the network connection may be abnormal.", + "accountPunishments": "Punishments", + "accountPunishmentsDescription": "View your account's reputation status.", + "punishmentType0": "Strike", + "punishmentType1": "Limited", + "punishmentType2": "Banned", + "punishmentOverall": "Overall Status", + "punishmentStatusNormal": "All abilities normal", + "punishmentStatusWarned": "All abilities normal, but at least one strike is in effect", + "punishmentStatusLimited": "Some abilities limited, at least one limited punishment is in effect", + "punishmentStatusLimitedFully": "All abilities limited, at least one completely limited punishment is in effect", + "punishmentStatusBanned": "All services are terminated, banned", + "punishmentCreatedAt": "Applied since {}", + "punishmentExpiredAt": "Expired at {}", + "punishmentExpiredNever": "Never expired", + "punishmentModerator": "Moderator who made this punishment", + "punishmentMadeBySystem": "Made by auto-mod system" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1e2bf05..2e08e69 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -914,5 +914,21 @@ "accountProgramLeft": "已离开计划。", "accountProgramAlreadyJoined": "已加入", "leave": "离开", - "attachmentFailedToLoadMedia": "无法加载媒体文件,请稍后重试。若此错误重复出现,可能源文件不存在或者网络连接异常。" + "attachmentFailedToLoadMedia": "无法加载媒体文件,请稍后重试。若此错误重复出现,可能源文件不存在或者网络连接异常。", + "accountPunishments": "处分", + "accountPunishmentsDescription": "查看你帐号的信誉状态。", + "punishmentType0": "警告", + "punishmentType1": "停权", + "punishmentType2": "封禁", + "punishmentOverall": "总体状态", + "punishmentStatusNormal": "所有功能正常", + "punishmentStatusWarned": "所有功能正常,但有警告生效", + "punishmentStatusLimited": "部份功能暂时受限,有至少一个停权生效", + "punishmentStatusLimitedFully": "所有功能暂时受限,有至少一个完全停权生效", + "punishmentStatusBanned": "所有服务终止,已被封禁", + "punishmentCreatedAt": "宣布于 {}", + "punishmentExpiredAt": "到期于 {}", + "punishmentExpiredNever": "永久生效", + "punishmentModerator": "责任管理员", + "punishmentMadeBySystem": "由系统自动裁决" } diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index 30d1a2d..ec8e037 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -6,6 +6,20 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:surface/types/realm.dart'; +class AppNavListItem { + final String title; + final String subtitle; + final String screen; + final IconData icon; + + const AppNavListItem({ + required this.title, + required this.subtitle, + required this.screen, + required this.icon, + }); +} + class AppNavDestination { final String label; final String screen; diff --git a/lib/router.dart b/lib/router.dart index 21023c8..e8a8926 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/account.dart'; -import 'package:surface/screens/account/account_settings.dart'; +import 'package:surface/screens/account/punishments.dart'; +import 'package:surface/screens/account/settings.dart'; import 'package:surface/screens/account/action_events.dart'; import 'package:surface/screens/account/badges.dart'; import 'package:surface/screens/account/contact_methods.dart'; @@ -131,6 +132,11 @@ final _appRoutes = [ name: 'account', builder: (context, state) => const AccountScreen(), routes: [ + GoRoute( + path: '/punishments', + name: 'accountPunishments', + builder: (context, state) => const PunishmentsScreen(), + ), GoRoute( path: '/programs', name: 'accountProgram', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index f2d0643..8fa83e1 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/database.dart'; +import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/websocket.dart'; @@ -22,6 +23,87 @@ import 'package:surface/widgets/universal_image.dart'; class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); + static const List kNavList = [ + AppNavListItem( + title: "accountPublishers", + subtitle: "accountPublishersSubtitle", + screen: "accountPublishers", + icon: Symbols.face, + ), + AppNavListItem( + title: "accountProgram", + subtitle: "accountProgramDescription", + screen: "accountProgram", + icon: Symbols.communities, + ), + AppNavListItem( + title: "friends", + subtitle: "friendsDescription", + screen: "friend", + icon: Symbols.person, + ), + AppNavListItem( + title: "album", + subtitle: "albumDescription", + screen: "album", + icon: Symbols.photo_library, + ), + AppNavListItem( + title: "stickers", + subtitle: "stickersDescription", + screen: "stickers", + icon: Symbols.emoji_emotions, + ), + AppNavListItem( + title: "accountWallet", + subtitle: "accountWalletSubtitle", + screen: "accountWallet", + icon: Symbols.wallet, + ), + AppNavListItem( + title: "accountBadges", + subtitle: "accountBadgesDescription", + screen: "accountBadges", + icon: Symbols.award_star, + ), + AppNavListItem( + title: "accountKeyPairs", + subtitle: "accountKeyPairsDescription", + screen: "accountKeyPairs", + icon: Symbols.key, + ), + AppNavListItem( + title: "accountPunishments", + subtitle: "accountPunishmentsDescription", + screen: "accountPunishments", + icon: Symbols.credit_score, + ), + AppNavListItem( + title: "accountActionEvent", + subtitle: "accountActionEventDescription", + screen: "accountActionEvents", + icon: Symbols.history, + ), + AppNavListItem( + title: "accountAuthTickets", + subtitle: "accountAuthTicketsDescription", + screen: "accountAuthTickets", + icon: Symbols.confirmation_number, + ), + AppNavListItem( + title: "accountSettings", + subtitle: "accountSettingsSubtitle", + screen: "accountSettings", + icon: Symbols.manage_accounts, + ), + AppNavListItem( + title: "abuseReport", + subtitle: "abuseReportActionDescription", + screen: "abuseReport", + icon: Symbols.flag, + ), + ]; + @override Widget build(BuildContext context) { final ua = context.watch(); @@ -146,145 +228,42 @@ class _AuthorizedAccountScreen extends StatelessWidget { ); }).padding(all: 20), ).padding(horizontal: 8, top: 16, bottom: 4), - ListTile( - title: Text('accountPublishers').tr(), - subtitle: Text('accountPublishersSubtitle').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.face), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountPublishers'); - }, - ), - ListTile( - title: Text('accountProgram').tr(), - subtitle: Text('accountProgramDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.communities), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountProgram'); - }, - ), - ListTile( - title: Text('friends').tr(), - subtitle: Text('friendsDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.person), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('friend'); - }, - ), - ListTile( - title: Text('album').tr(), - subtitle: Text('albumDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.photo_library), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('album'); - }, - ), - ListTile( - title: Text('stickers').tr(), - subtitle: Text('stickersDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.emoji_emotions), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('stickers'); - }, - ), - ListTile( - title: Text('accountWallet').tr(), - subtitle: Text('accountWalletSubtitle').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.wallet), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountWallet'); - }, - ), - ListTile( - title: Text('accountBadges').tr(), - subtitle: Text('accountBadgesDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.award_star), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountBadges'); - }, - ), - ListTile( - title: Text('accountKeyPairs').tr(), - subtitle: Text('accountKeyPairsDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.key), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountKeyPairs'); - }, - ), - ListTile( - title: Text('accountActionEvent').tr(), - subtitle: Text('accountActionEventDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.history), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountActionEvents'); - }, - ), - ListTile( - title: Text('accountAuthTickets').tr(), - subtitle: Text('accountAuthTicketsDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.confirmation_number), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountAuthTickets'); - }, - ), - ListTile( - title: Text('accountSettings').tr(), - subtitle: Text('accountSettingsSubtitle').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.manage_accounts), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('accountSettings'); - }, - ), - ListTile( - title: Text('abuseReport').tr(), - subtitle: Text('abuseReportActionDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.flag), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed('abuseReport'); - }, - ), - ListTile( - title: Text('accountLogout').tr(), - subtitle: Text('accountLogoutSubtitle').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.logout), - trailing: const Icon(Symbols.chevron_right), - onTap: () async { - final confirm = await context.showConfirmDialog( - 'accountLogoutConfirmTitle'.tr(), - 'accountLogoutConfirm'.tr(), - ); + for (final item in AccountScreen.kNavList) + Tooltip( + message: item.subtitle.tr(), + child: ListTile( + minTileHeight: 48, + title: Text(item.title).tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: Icon(item.icon), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed(item.screen); + }, + ), + ), + Tooltip( + message: 'accountLogoutSubtitle'.tr(), + child: ListTile( + title: Text('accountLogout').tr(), + minTileHeight: 48, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.logout), + trailing: const Icon(Symbols.chevron_right), + onTap: () async { + final confirm = await context.showConfirmDialog( + 'accountLogoutConfirmTitle'.tr(), + 'accountLogoutConfirm'.tr(), + ); - if (!confirm) return; - if (!context.mounted) return; - ua.logoutUser(); - final ws = context.read(); - ws.disconnect(); - context.read().removeDatabase(); - }, + if (!confirm) return; + if (!context.mounted) return; + ua.logoutUser(); + final ws = context.read(); + ws.disconnect(); + context.read().removeDatabase(); + }, + ), ), ], ); diff --git a/lib/screens/account/punishments.dart b/lib/screens/account/punishments.dart new file mode 100644 index 0000000..5700b8a --- /dev/null +++ b/lib/screens/account/punishments.dart @@ -0,0 +1,180 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/account.dart'; +import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; + +const kPunishmentIcons = [ + Symbols.warning, + Symbols.emergency_home, + Symbols.dangerous, +]; + +class PunishmentsScreen extends StatefulWidget { + const PunishmentsScreen({super.key}); + + @override + State createState() => _PunishmentsScreenState(); +} + +class _PunishmentsScreenState extends State { + bool _isBusy = false; + List? _punishments; + + Future _fetchPunishments() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/punishments'); + if (!mounted) return; + _punishments = List.from( + resp.data.map((ele) => SnPunishment.fromJson(ele)), + ); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchPunishments(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + title: Text('accountPunishments').tr(), + leading: PageBackButton(), + ), + body: Column( + children: [ + LoadingIndicator(isActive: _isBusy), + Card( + margin: EdgeInsets.only(bottom: 8, left: 8, right: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Symbols.visibility, size: 20), + const Gap(6), + Expanded( + child: Text('punishmentOverall').tr().fontSize(16).bold(), + ), + ], + ), + Builder( + builder: (context) { + if (_punishments == null) return Text('loading').tr(); + if (_punishments!.any((ele) => ele.type == 2)) { + return Text('punishmentStatusBanned').tr(); + } + if (_punishments!.any( + (ele) => ele.type == 1 && ele.permNodes.isEmpty, + )) { + return Text('punishmentStatusLimitedFully').tr(); + } else if (_punishments!.any((ele) => ele.type == 1)) { + return Text('punishmentStatusLimited').tr(); + } + if (_punishments!.any((ele) => ele.type == 0)) { + return Text('punishmentStatusWarned').tr(); + } + return Text('punishmentStatusNormal').tr(); + }, + ), + ], + ).padding(horizontal: 24, vertical: 16), + ), + Expanded( + child: RefreshIndicator( + onRefresh: _fetchPunishments, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: _punishments?.length ?? 0, + itemBuilder: (context, index) { + final ele = _punishments![index]; + return Card( + margin: EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(kPunishmentIcons[ele.type], size: 20), + const Gap(6), + Expanded( + child: Text('punishmentType${ele.type}') + .tr() + .fontSize(16) + .bold(), + ), + ], + ), + Text(ele.reason), + const Gap(4), + Text( + 'punishmentCreatedAt' + .tr(args: [DateFormat().format(ele.createdAt)]), + ).opacity(0.8), + Text( + ele.expiredAt == null + ? 'punishmentExpiredNever'.tr() + : 'punishmentExpiredAt'.tr( + args: [DateFormat().format(ele.expiredAt!)]), + ).opacity(0.8), + const Gap(8), + if (ele.moderator != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('punishmentModerator').tr().opacity(0.75), + InkWell( + child: Row( + children: [ + AccountImage( + content: ele.moderator!.avatar, + radius: 8, + ), + const Gap(4), + Text(ele.moderator?.nick ?? 'unknown'), + ], + ), + onTap: () { + GoRouter.of(context).pushNamed( + 'accountProfilePage', + pathParameters: { + 'name': ele.moderator!.name, + }, + ); + }, + ), + ], + ) + else + Text('punishmentMadeBySystem').tr().opacity(0.75), + ], + ).padding(horizontal: 24, vertical: 16), + ); + }, + separatorBuilder: (_, __) => const Gap(8), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/account/account_settings.dart b/lib/screens/account/settings.dart similarity index 100% rename from lib/screens/account/account_settings.dart rename to lib/screens/account/settings.dart diff --git a/lib/screens/friend.dart b/lib/screens/friend.dart index 38c150c..c53681c 100644 --- a/lib/screens/friend.dart +++ b/lib/screens/friend.dart @@ -10,7 +10,6 @@ import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/account.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_select.dart'; -import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; @@ -46,7 +45,8 @@ class _FriendScreenState extends State { try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=1'); - _relations = List.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); + _relations = List.from( + resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -64,7 +64,8 @@ class _FriendScreenState extends State { try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3'); - _requests = List.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); + _requests = List.from( + resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -82,7 +83,8 @@ class _FriendScreenState extends State { try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=2'); - _blocks = List.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); + _blocks = List.from( + resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -98,7 +100,8 @@ class _FriendScreenState extends State { try { final rel = context.read(); - await rel.updateRelationship(relation.relatedId, dstStatus, relation.permNodes); + await rel.updateRelationship( + relation.relatedId, dstStatus, relation.permNodes); if (!mounted) return; _fetchRelations(); } catch (err) { @@ -112,7 +115,8 @@ class _FriendScreenState extends State { Future _deleteRelation(SnRelationship relation) async { final confirm = await context.showConfirmDialog( 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), - 'friendDeleteDescription'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), + 'friendDeleteDescription' + .tr(args: [relation.related?.nick ?? 'unknown'.tr()]), ); if (!confirm) return; if (!mounted) return; @@ -133,7 +137,10 @@ class _FriendScreenState extends State { } void _showRequests() { - showModalBottomSheet(context: context, builder: (context) => _FriendshipListWidget(relations: _requests)).then(( + showModalBottomSheet( + context: context, + builder: (context) => _FriendshipListWidget(relations: _requests)) + .then(( value, ) { if (value != null) { @@ -144,7 +151,9 @@ class _FriendScreenState extends State { } void _showBlocks() { - showModalBottomSheet(context: context, builder: (context) => _FriendshipListWidget(relations: _blocks)).then(( + showModalBottomSheet( + context: context, + builder: (context) => _FriendshipListWidget(relations: _blocks)).then(( value, ) { if (value != null) { @@ -159,7 +168,8 @@ class _FriendScreenState extends State { try { final sn = context.read(); - await sn.client.post('/cgi/id/users/me/relations', data: {'related': user.name}); + await sn.client + .post('/cgi/id/users/me/relations', data: {'related': user.name}); if (!mounted) return; context.showSnackbar('friendRequestSent'.tr()); } catch (err) { @@ -184,13 +194,19 @@ class _FriendScreenState extends State { if (!ua.isAuthorized) { return AppScaffold( - appBar: AppBar(leading: PageBackButton(), title: Text('screenFriend').tr()), + appBar: AppBar( + leading: PageBackButton(), + title: Text('screenFriend').tr(), + ), body: Center(child: UnauthorizedHint()), ); } return AppScaffold( - appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenFriend').tr()), + appBar: AppBar( + leading: PageBackButton(), + title: Text('screenFriend').tr(), + ), floatingActionButton: FloatingActionButton( child: const Icon(Symbols.add), onPressed: () async { @@ -209,7 +225,8 @@ class _FriendScreenState extends State { if (_requests.isNotEmpty) ListTile( title: Text('friendRequests').tr(), - subtitle: Text('friendRequestsDescription').plural(_requests.length), + subtitle: + Text('friendRequestsDescription').plural(_requests.length), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.group_add), trailing: const Icon(Symbols.chevron_right), @@ -218,19 +235,22 @@ class _FriendScreenState extends State { if (_blocks.isNotEmpty) ListTile( title: Text('friendBlocklist').tr(), - subtitle: Text('friendBlocklistDescription').plural(_blocks.length), + subtitle: + Text('friendBlocklistDescription').plural(_blocks.length), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.block), trailing: const Icon(Symbols.chevron_right), onTap: _showBlocks, ), - if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), + if (_requests.isNotEmpty || _blocks.isNotEmpty) + const Divider(height: 1), Expanded( child: MediaQuery.removePadding( context: context, removeTop: true, child: RefreshIndicator( - onRefresh: () => Future.wait([_fetchRelations(), _fetchRequests()]), + onRefresh: () => + Future.wait([_fetchRelations(), _fetchRequests()]), child: ListView.builder( itemCount: _relations.length, itemBuilder: (context, index) { @@ -254,12 +274,16 @@ class _FriendScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( - onTap: _isUpdating ? null : () => _changeRelation(relation, 2), + onTap: _isUpdating + ? null + : () => _changeRelation(relation, 2), child: Text('friendBlock').tr(), ), const Gap(8), InkWell( - onTap: _isUpdating ? null : () => _deleteRelation(relation), + onTap: _isUpdating + ? null + : () => _deleteRelation(relation), child: Text('friendDeleteAction').tr(), ), ], @@ -328,7 +352,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { try { final rel = context.read(); - await rel.updateRelationship(relation.relatedId, dstStatus, relation.permNodes); + await rel.updateRelationship( + relation.relatedId, dstStatus, relation.permNodes); if (!mounted) return; Navigator.pop(context, true); } catch (err) { @@ -342,7 +367,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { Future _deleteRelation(SnRelationship relation) async { final confirm = await context.showConfirmDialog( 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), - 'friendDeleteDescription'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), + 'friendDeleteDescription' + .tr(args: [relation.related?.nick ?? 'unknown'.tr()]), ); if (!confirm) return; if (!mounted) return; @@ -382,7 +408,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), + Text(kFriendStatus[relation.status] ?? 'unknown') + .tr() + .opacity(0.75), if (relation.status == 0) Row( mainAxisAlignment: MainAxisAlignment.end, @@ -403,7 +431,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( - onTap: _isBusy ? null : () => _changeRelation(relation, 1), + onTap: + _isBusy ? null : () => _changeRelation(relation, 1), child: Text('friendUnblock').tr(), ), const Gap(8), diff --git a/lib/types/account.dart b/lib/types/account.dart index 15376c1..d7a5458 100644 --- a/lib/types/account.dart +++ b/lib/types/account.dart @@ -223,3 +223,24 @@ abstract class SnProgramMember with _$SnProgramMember { factory SnProgramMember.fromJson(Map json) => _$SnProgramMemberFromJson(json); } + +@freezed +abstract class SnPunishment with _$SnPunishment { + const factory SnPunishment({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required String reason, + required int type, + @Default({}) Map permNodes, + required DateTime? expiredAt, + required SnAccount? account, + required int? accountId, + required SnAccount? moderator, + required int? moderatorId, + }) = _SnPunishment; + + factory SnPunishment.fromJson(Map json) => + _$SnPunishmentFromJson(json); +} diff --git a/lib/types/account.freezed.dart b/lib/types/account.freezed.dart index 3ba3fb6..8e60ea5 100644 --- a/lib/types/account.freezed.dart +++ b/lib/types/account.freezed.dart @@ -4229,4 +4229,461 @@ class __$SnProgramMemberCopyWithImpl<$Res> } } +/// @nodoc +mixin _$SnPunishment { + int get id; + DateTime get createdAt; + DateTime get updatedAt; + DateTime? get deletedAt; + String get reason; + int get type; + Map get permNodes; + DateTime? get expiredAt; + SnAccount? get account; + int? get accountId; + SnAccount? get moderator; + int? get moderatorId; + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $SnPunishmentCopyWith get copyWith => + _$SnPunishmentCopyWithImpl( + this as SnPunishment, _$identity); + + /// Serializes this SnPunishment to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is SnPunishment && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.reason, reason) || other.reason == reason) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other.permNodes, permNodes) && + (identical(other.expiredAt, expiredAt) || + other.expiredAt == expiredAt) && + (identical(other.account, account) || other.account == account) && + (identical(other.accountId, accountId) || + other.accountId == accountId) && + (identical(other.moderator, moderator) || + other.moderator == moderator) && + (identical(other.moderatorId, moderatorId) || + other.moderatorId == moderatorId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + reason, + type, + const DeepCollectionEquality().hash(permNodes), + expiredAt, + account, + accountId, + moderator, + moderatorId); + + @override + String toString() { + return 'SnPunishment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, reason: $reason, type: $type, permNodes: $permNodes, expiredAt: $expiredAt, account: $account, accountId: $accountId, moderator: $moderator, moderatorId: $moderatorId)'; + } +} + +/// @nodoc +abstract mixin class $SnPunishmentCopyWith<$Res> { + factory $SnPunishmentCopyWith( + SnPunishment value, $Res Function(SnPunishment) _then) = + _$SnPunishmentCopyWithImpl; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String reason, + int type, + Map permNodes, + DateTime? expiredAt, + SnAccount? account, + int? accountId, + SnAccount? moderator, + int? moderatorId}); + + $SnAccountCopyWith<$Res>? get account; + $SnAccountCopyWith<$Res>? get moderator; +} + +/// @nodoc +class _$SnPunishmentCopyWithImpl<$Res> implements $SnPunishmentCopyWith<$Res> { + _$SnPunishmentCopyWithImpl(this._self, this._then); + + final SnPunishment _self; + final $Res Function(SnPunishment) _then; + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? reason = null, + Object? type = null, + Object? permNodes = null, + Object? expiredAt = freezed, + Object? account = freezed, + Object? accountId = freezed, + Object? moderator = freezed, + Object? moderatorId = freezed, + }) { + return _then(_self.copyWith( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _self.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _self.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + reason: null == reason + ? _self.reason + : reason // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as int, + permNodes: null == permNodes + ? _self.permNodes + : permNodes // ignore: cast_nullable_to_non_nullable + as Map, + expiredAt: freezed == expiredAt + ? _self.expiredAt + : expiredAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + account: freezed == account + ? _self.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount?, + accountId: freezed == accountId + ? _self.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int?, + moderator: freezed == moderator + ? _self.moderator + : moderator // ignore: cast_nullable_to_non_nullable + as SnAccount?, + moderatorId: freezed == moderatorId + ? _self.moderatorId + : moderatorId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res>? get account { + if (_self.account == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.account!, (value) { + return _then(_self.copyWith(account: value)); + }); + } + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res>? get moderator { + if (_self.moderator == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.moderator!, (value) { + return _then(_self.copyWith(moderator: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _SnPunishment implements SnPunishment { + const _SnPunishment( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.reason, + required this.type, + final Map permNodes = const {}, + required this.expiredAt, + required this.account, + required this.accountId, + required this.moderator, + required this.moderatorId}) + : _permNodes = permNodes; + factory _SnPunishment.fromJson(Map json) => + _$SnPunishmentFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final DateTime? deletedAt; + @override + final String reason; + @override + final int type; + final Map _permNodes; + @override + @JsonKey() + Map get permNodes { + if (_permNodes is EqualUnmodifiableMapView) return _permNodes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_permNodes); + } + + @override + final DateTime? expiredAt; + @override + final SnAccount? account; + @override + final int? accountId; + @override + final SnAccount? moderator; + @override + final int? moderatorId; + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$SnPunishmentCopyWith<_SnPunishment> get copyWith => + __$SnPunishmentCopyWithImpl<_SnPunishment>(this, _$identity); + + @override + Map toJson() { + return _$SnPunishmentToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _SnPunishment && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.reason, reason) || other.reason == reason) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality() + .equals(other._permNodes, _permNodes) && + (identical(other.expiredAt, expiredAt) || + other.expiredAt == expiredAt) && + (identical(other.account, account) || other.account == account) && + (identical(other.accountId, accountId) || + other.accountId == accountId) && + (identical(other.moderator, moderator) || + other.moderator == moderator) && + (identical(other.moderatorId, moderatorId) || + other.moderatorId == moderatorId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + reason, + type, + const DeepCollectionEquality().hash(_permNodes), + expiredAt, + account, + accountId, + moderator, + moderatorId); + + @override + String toString() { + return 'SnPunishment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, reason: $reason, type: $type, permNodes: $permNodes, expiredAt: $expiredAt, account: $account, accountId: $accountId, moderator: $moderator, moderatorId: $moderatorId)'; + } +} + +/// @nodoc +abstract mixin class _$SnPunishmentCopyWith<$Res> + implements $SnPunishmentCopyWith<$Res> { + factory _$SnPunishmentCopyWith( + _SnPunishment value, $Res Function(_SnPunishment) _then) = + __$SnPunishmentCopyWithImpl; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String reason, + int type, + Map permNodes, + DateTime? expiredAt, + SnAccount? account, + int? accountId, + SnAccount? moderator, + int? moderatorId}); + + @override + $SnAccountCopyWith<$Res>? get account; + @override + $SnAccountCopyWith<$Res>? get moderator; +} + +/// @nodoc +class __$SnPunishmentCopyWithImpl<$Res> + implements _$SnPunishmentCopyWith<$Res> { + __$SnPunishmentCopyWithImpl(this._self, this._then); + + final _SnPunishment _self; + final $Res Function(_SnPunishment) _then; + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? reason = null, + Object? type = null, + Object? permNodes = null, + Object? expiredAt = freezed, + Object? account = freezed, + Object? accountId = freezed, + Object? moderator = freezed, + Object? moderatorId = freezed, + }) { + return _then(_SnPunishment( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _self.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _self.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + reason: null == reason + ? _self.reason + : reason // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as int, + permNodes: null == permNodes + ? _self._permNodes + : permNodes // ignore: cast_nullable_to_non_nullable + as Map, + expiredAt: freezed == expiredAt + ? _self.expiredAt + : expiredAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + account: freezed == account + ? _self.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount?, + accountId: freezed == accountId + ? _self.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int?, + moderator: freezed == moderator + ? _self.moderator + : moderator // ignore: cast_nullable_to_non_nullable + as SnAccount?, + moderatorId: freezed == moderatorId + ? _self.moderatorId + : moderatorId // ignore: cast_nullable_to_non_nullable + as int?, + )); + } + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res>? get account { + if (_self.account == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.account!, (value) { + return _then(_self.copyWith(account: value)); + }); + } + + /// Create a copy of SnPunishment + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res>? get moderator { + if (_self.moderator == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.moderator!, (value) { + return _then(_self.copyWith(moderator: value)); + }); + } +} + // dart format on diff --git a/lib/types/account.g.dart b/lib/types/account.g.dart index e5bee8d..d0b1018 100644 --- a/lib/types/account.g.dart +++ b/lib/types/account.g.dart @@ -380,3 +380,43 @@ Map _$SnProgramMemberToJson(_SnProgramMember instance) => 'program': instance.program.toJson(), 'program_id': instance.programId, }; + +_SnPunishment _$SnPunishmentFromJson(Map json) => + _SnPunishment( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + reason: json['reason'] as String, + type: (json['type'] as num).toInt(), + permNodes: json['perm_nodes'] as Map? ?? const {}, + expiredAt: json['expired_at'] == null + ? null + : DateTime.parse(json['expired_at'] as String), + account: json['account'] == null + ? null + : SnAccount.fromJson(json['account'] as Map), + accountId: (json['account_id'] as num?)?.toInt(), + moderator: json['moderator'] == null + ? null + : SnAccount.fromJson(json['moderator'] as Map), + moderatorId: (json['moderator_id'] as num?)?.toInt(), + ); + +Map _$SnPunishmentToJson(_SnPunishment instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'reason': instance.reason, + 'type': instance.type, + 'perm_nodes': instance.permNodes, + 'expired_at': instance.expiredAt?.toIso8601String(), + 'account': instance.account?.toJson(), + 'account_id': instance.accountId, + 'moderator': instance.moderator?.toJson(), + 'moderator_id': instance.moderatorId, + };