From 58421e5d5e90b3d61058e89d41c9e22ad6962f3f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Mar 2025 01:39:05 +0800 Subject: [PATCH] :sparkles: Basic contact methods --- api/Passport/Deal Abuse Report.bru | 6 +- assets/translations/en-US.json | 15 +- assets/translations/zh-CN.json | 15 +- assets/translations/zh-HK.json | 15 +- assets/translations/zh-TW.json | 15 +- lib/router.dart | 6 + lib/screens/account/account_settings.dart | 10 + lib/screens/account/contact_methods.dart | 292 ++++++++++++++++++++++ 8 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 lib/screens/account/contact_methods.dart diff --git a/api/Passport/Deal Abuse Report.bru b/api/Passport/Deal Abuse Report.bru index adeea85..706babe 100644 --- a/api/Passport/Deal Abuse Report.bru +++ b/api/Passport/Deal Abuse Report.bru @@ -5,14 +5,14 @@ meta { } put { - url: {{endpoint}}/cgi/id/reports/abuse/3/status + url: {{endpoint}}/cgi/id/reports/abuse/6/status body: json auth: inherit } body:json { { - "status": "processed", - "message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。" + "status": "rejected", + "message": "Not a good reason" } } diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index e72bfff..16689f3 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -822,5 +822,18 @@ "accountUnconfirmedResendSuccessful": "Email has been resent, you can resend it again in 60 minutes.", "stickerPickerEmpty": "Sticker list is empty", "stickerPickerEmptyHint": "To start using stickers, you need to add a sticker pack first.", - "goto": "Go to {}" + "goto": "Go to {}", + "accountContactMethods": "Contact Methods", + "accountContactMethodsDescription": "Manage your contact methods.", + "accountContactMethodsNameEmail": "Email address", + "accountContactMethodsNamePhone": "Phone number", + "accountContactMethodsNameAddress": "Address", + "accountContactMethodsPrimary": "Primary", + "accountContactMethodsVerified": "Verified", + "accountContactMethodsPublic": "Public", + "accountContactMethodsAdd": "Add Contact Method", + "accountContactMethodsEdit": "Edit Contact Method", + "accountContactMethodsAddDescription": "Add a new contact method.", + "fieldContactContent": "Contact method", + "accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 86baa9c..c540df9 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -822,5 +822,18 @@ "accountUnconfirmedResendSuccessful": "邮件已重新发送,你可以在 60 分钟后再重发一封。", "stickerPickerEmpty": "贴图列表为空", "stickerPickerEmptyHint": "想要开始使用贴图,请先添加贴图包。", - "goto": "跳转到 {}" + "goto": "跳转到 {}", + "accountContactMethods": "联系方式", + "accountContactMethodsDescription": "管理你的联系方式。", + "accountContactMethodsNameEmail": "电子邮箱", + "accountContactMethodsNamePhone": "电话", + "accountContactMethodsNameAddress": "地址", + "accountContactMethodsPrimary": "主要的", + "accountContactMethodsVerified": "已验证", + "accountContactMethodsPublic": "公开的", + "accountContactMethodsAdd": "添加联系方式", + "accountContactMethodsEdit": "编辑联系方式", + "accountContactMethodsAddDescription": "添加新的联系方式。", + "fieldContactContent": "联系方式", + "accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 223fc87..eb1ce2e 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -822,5 +822,18 @@ "accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。", "stickerPickerEmpty": "貼圖列表為空", "stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。", - "goto": "跳轉到 {}" + "goto": "跳轉到 {}", + "accountContactMethods": "聯繫方式", + "accountContactMethodsDescription": "管理你的聯繫方式。", + "accountContactMethodsNameEmail": "電子郵箱", + "accountContactMethodsNamePhone": "電話", + "accountContactMethodsNameAddress": "地址", + "accountContactMethodsPrimary": "主要的", + "accountContactMethodsVerified": "已驗證", + "accountContactMethodsPublic": "公開的", + "accountContactMethodsAdd": "添加聯繫方式", + "accountContactMethodsEdit": "編輯聯繫方式", + "accountContactMethodsAddDescription": "添加新的聯繫方式。", + "fieldContactContent": "聯繫方式", + "accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 2587bfd..dd01073 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -822,5 +822,18 @@ "accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。", "stickerPickerEmpty": "貼圖列表為空", "stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。", - "goto": "跳轉到 {}" + "goto": "跳轉到 {}", + "accountContactMethods": "聯繫方式", + "accountContactMethodsDescription": "管理你的聯繫方式。", + "accountContactMethodsNameEmail": "電子郵箱", + "accountContactMethodsNamePhone": "電話", + "accountContactMethodsNameAddress": "地址", + "accountContactMethodsPrimary": "主要的", + "accountContactMethodsVerified": "已驗證", + "accountContactMethodsPublic": "公開的", + "accountContactMethodsAdd": "添加聯繫方式", + "accountContactMethodsEdit": "編輯聯繫方式", + "accountContactMethodsAddDescription": "添加新的聯繫方式。", + "fieldContactContent": "聯繫方式", + "accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。" } diff --git a/lib/router.dart b/lib/router.dart index 7c32e7b..5b04c8a 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -6,6 +6,7 @@ import 'package:surface/screens/account.dart'; import 'package:surface/screens/account/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'; import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/screens/account/keypairs.dart'; import 'package:surface/screens/account/profile_page.dart'; @@ -126,6 +127,11 @@ final _appRoutes = [ name: 'account', builder: (context, state) => const AccountScreen(), routes: [ + GoRoute( + path: '/contacts', + name: 'accountContactMethods', + builder: (context, state) => const AccountContactMethod(), + ), GoRoute( path: '/events', name: 'accountActionEvents', diff --git a/lib/screens/account/account_settings.dart b/lib/screens/account/account_settings.dart index 25d94f5..6454e1a 100644 --- a/lib/screens/account/account_settings.dart +++ b/lib/screens/account/account_settings.dart @@ -87,6 +87,16 @@ class AccountSettingsScreen extends StatelessWidget { ), ), ), + ListTile( + title: Text('accountContactMethods').tr(), + subtitle: Text('accountContactMethodsDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.contacts), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('accountContactMethods'); + }, + ), ListTile( title: Text('accountProfileEdit').tr(), subtitle: Text('accountProfileEditSubtitle').tr(), diff --git a/lib/screens/account/contact_methods.dart b/lib/screens/account/contact_methods.dart new file mode 100644 index 0000000..53d6e64 --- /dev/null +++ b/lib/screens/account/contact_methods.dart @@ -0,0 +1,292 @@ +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.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/dialog.dart'; +import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; + +const kContactMethodsIcons = [Symbols.email, Symbols.phone, Symbols.map]; +const kContactMethodsName = ['Email', 'Phone', 'Address']; + +class AccountContactMethod extends StatefulWidget { + const AccountContactMethod({super.key}); + + @override + State createState() => _AccountContactMethodState(); +} + +class _AccountContactMethodState extends State { + bool _isBusy = false; + List _contactMethods = List.empty(growable: true); + + Future _fetchContactMethods() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/users/me/contacts'); + _contactMethods = List.from((resp.data as List) + .map((e) => SnAccountContact.fromJson(e))); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchContactMethods(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('accountContactMethods').tr(), + ), + body: Column( + children: [ + LoadingIndicator(isActive: _isBusy), + ListTile( + title: Text('accountContactMethodsAdd').tr(), + subtitle: Text('accountContactMethodsAddDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.add), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + showDialog( + context: context, + builder: (context) => _ContactMethodEditor(), + ).then((value) { + if (value) { + _fetchContactMethods(); + } + }); + }, + ), + Divider(height: 1), + Expanded( + child: RefreshIndicator( + onRefresh: _fetchContactMethods, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: _contactMethods.length, + itemBuilder: (context, index) { + final method = _contactMethods[index]; + return ListTile( + title: Text(method.content), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'accountContactMethodsName${kContactMethodsName[method.type]}', + ).tr().bold(), + if (method.isPrimary || + method.isPublic || + method.verifiedAt != null) + Row( + spacing: 4, + children: [ + if (method.isPrimary) + Text('accountContactMethodsPrimary').tr(), + if (method.isPublic) + Text('accountContactMethodsPublic').tr(), + if (method.verifiedAt != null) + Text('accountContactMethodsVerified').tr(), + ], + ), + ], + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: Icon( + kContactMethodsIcons[method.type], + ), + trailing: PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.edit), + const Gap(8), + Text('edit').tr(), + ], + ), + onTap: () { + showDialog( + context: context, + builder: (context) => _ContactMethodEditor( + contact: method, + ), + ).then((value) { + if (value) { + _fetchContactMethods(); + } + }); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} + +class _ContactMethodEditor extends StatefulWidget { + final SnAccountContact? contact; + const _ContactMethodEditor({this.contact}); + + @override + State<_ContactMethodEditor> createState() => _ContactMethodEditorState(); +} + +class _ContactMethodEditorState extends State<_ContactMethodEditor> { + int _type = 0; + bool _isPublic = false; + final TextEditingController _contentController = TextEditingController(); + + bool _isBusy = false; + + Future _saveContactMethod() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + await sn.client.request( + widget.contact == null + ? '/cgi/id/users/me/contacts' + : '/cgi/id/users/me/contacts/${widget.contact!.id}', + data: { + 'content': _contentController.text, + 'type': _type, + 'is_public': _isPublic, + }, + options: Options( + method: widget.contact == null ? 'POST' : 'PUT', + ), + ); + if (!mounted) return; + Navigator.pop(context, true); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + if (widget.contact != null) { + _type = widget.contact!.type; + _isPublic = widget.contact!.isPublic; + _contentController.text = widget.contact!.content; + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: widget.contact == null + ? Text('accountContactMethodsAdd').tr() + : Text('accountContactMethodsEdit').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _type, + items: kContactMethodsName + .mapIndexed((idx, ele) => DropdownMenuItem( + value: idx, + child: Text('accountContactMethodsName$ele').tr(), + )) + .toList(), + buttonStyleData: ButtonStyleData( + height: 48, + width: double.infinity, + padding: const EdgeInsets.only(left: 14, right: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context).dividerColor, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + height: 48, + padding: EdgeInsets.only(left: 14, right: 14), + ), + onChanged: (value) { + setState(() => _type = value ?? 0); + }, + ), + ), + const Gap(8), + TextField( + controller: _contentController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'fieldContactContent'.tr(), + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(8), + Card( + margin: EdgeInsets.zero, + child: CheckboxListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + ), + title: Text('accountContactMethodsPublic').tr(), + subtitle: Text('accountContactMethodsPublicHint').tr(), + secondary: const Icon(Symbols.globe), + value: _isPublic, + onChanged: (value) { + setState(() => _isPublic = value ?? false); + }, + ), + ) + ], + ), + actions: [ + TextButton( + onPressed: _isBusy + ? null + : () { + Navigator.of(context).pop(); + }, + child: Text('dialogDismiss').tr(), + ), + TextButton( + onPressed: _isBusy + ? null + : () { + _saveContactMethod(); + }, + child: Text('dialogConfirm').tr(), + ), + ], + ); + } +}