From 000caf4dd212f1ecee83185d858e8381833dddc9 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 1 Dec 2024 14:33:47 +0800 Subject: [PATCH] :sparkles: Publisher personal & organization management --- assets/translations/en.json | 2 + assets/translations/zh.json | 2 + .../account/publishers/publisher_edit.dart | 13 +- .../account/publishers/publisher_new.dart | 329 +++++++++++++++++- 4 files changed, 334 insertions(+), 12 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 05d8488..4e983bd 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -99,6 +99,8 @@ "publishersNew": "New Publisher", "publisherNewSubtitle": "Create a new publisher identity.", "publisherSyncWithAccount": "Sync with account", + "fieldPublisherBelongToRealm": "Belongs to", + "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm", "writePostTypeStory": "Post a story", "writePostTypeArticle": "Write an article", "fieldPostPublisher": "Post publisher", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 69eaaf9..3a7459a 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -99,6 +99,8 @@ "publishersNew": "新发布者", "publisherNewSubtitle": "创建一个新的公共身份。", "publisherSyncWithAccount": "同步账户信息", + "fieldPublisherBelongToRealm": "所属领域", + "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域", "writePostTypeStory": "发动态", "writePostTypeArticle": "写文章", "fieldPostPublisher": "帖子发布者", diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index 06d6e8d..475ef92 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -267,11 +267,14 @@ class _AccountPublisherEditScreenState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TextButton.icon( - onPressed: _syncWithAccount, - label: Text('publisherSyncWithAccount').tr(), - icon: const Icon(Symbols.sync), - ), + if (_publisher?.type == 0) + TextButton.icon( + onPressed: _syncWithAccount, + label: Text('publisherSyncWithAccount').tr(), + icon: const Icon(Symbols.sync), + ) + else + const SizedBox(), ElevatedButton.icon( onPressed: _isBusy ? null : _performAction, label: Text('apply').tr(), diff --git a/lib/screens/account/publishers/publisher_new.dart b/lib/screens/account/publishers/publisher_new.dart index 98b34ab..b10664c 100644 --- a/lib/screens/account/publishers/publisher_new.dart +++ b/lib/screens/account/publishers/publisher_new.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; @@ -6,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; @@ -47,6 +49,7 @@ class _AccountPublisherNewScreenState extends State { ), switch (mode) { 'personal' => const _PublisherNewPersonal(), + 'organization' => const _PublisherNewOrganization(), _ => const Placeholder(), }, ], @@ -66,6 +69,10 @@ class _PublisherNewPersonal extends StatefulWidget { class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { bool _isBusy = false; + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _nickController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + void _performAction() async { final sn = context.read(); final ua = context.read(); @@ -74,15 +81,48 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { setState(() => _isBusy = true); try { - await sn.client.post('/cgi/co/publishers/personal'); + await sn.client.post('/cgi/co/publishers/personal', data: { + 'name': _nameController.text, + 'nick': _nickController.text, + 'description': _descriptionController.text, + 'avatar': ua.user!.avatar, + 'banner': ua.user!.banner, + }); + if (!mounted) return; Navigator.pop(context, true); } catch (err) { + if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } + void _syncState() { + final ua = context.read(); + if (ua.user == null) return; + + _nameController.text = ua.user!.name; + _nickController.text = ua.user!.nick; + _descriptionController.text = ua.user!.description; + } + + @override + void initState() { + super.initState(); + _syncState(); + _nameController.addListener(() => setState(() => {})); + _nickController.addListener(() => setState(() => {})); + } + + @override + void dispose() { + super.dispose(); + _nameController.dispose(); + _nickController.dispose(); + _descriptionController.dispose(); + } + @override Widget build(BuildContext context) { final ua = context.watch(); @@ -90,10 +130,41 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('preview') - .tr() - .textStyle(Theme.of(context).textTheme.titleMedium!) - .padding(horizontal: 16, vertical: 4), + Column( + children: [ + TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'fieldUsername'.tr(), + helperText: 'fieldUsernameCannotEditHint'.tr(), + helperMaxLines: 2, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(4), + TextField( + controller: _nickController, + decoration: InputDecoration( + labelText: 'fieldNickname'.tr(), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(4), + TextField( + controller: _descriptionController, + minLines: 3, + maxLines: null, + decoration: InputDecoration( + labelText: 'fieldDescription'.tr(), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ).padding(horizontal: 8), + const Gap(16), Card( child: SizedBox( width: double.infinity, @@ -105,10 +176,254 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text(ua.user!.nick) + Text(_nickController.text) .textStyle(Theme.of(context).textTheme.titleLarge!), const Gap(4), - Text('@${ua.user!.name}') + Text('@${_nameController.text}') + .textStyle(Theme.of(context).textTheme.bodySmall!), + ], + ), + ], + ), + ).padding(all: 16), + ), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _isBusy ? null : _performAction, + icon: const Icon(Symbols.add), + label: Text('create').tr(), + ), + ).padding(horizontal: 2), + ], + ); + } +} + +class _PublisherNewOrganization extends StatefulWidget { + const _PublisherNewOrganization({super.key}); + + @override + State<_PublisherNewOrganization> createState() => + _PublisherNewOrganizationState(); +} + +class _PublisherNewOrganizationState extends State<_PublisherNewOrganization> { + bool _isBusy = false; + + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _nickController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + + void _performAction() async { + final sn = context.read(); + final ua = context.read(); + if (!ua.isAuthorized) return; + if (_belongToRealm == null) return; + + setState(() => _isBusy = true); + + try { + await sn.client.post('/cgi/co/publishers/organization', data: { + 'realm': _belongToRealm!.alias, + 'name': _nameController.text, + 'nick': _nickController.text, + 'description': _descriptionController.text, + 'avatar': _belongToRealm!.avatar, + 'banner': _belongToRealm!.banner, + }); + if (!mounted) return; + Navigator.pop(context, true); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + List? _realms; + SnRealm? _belongToRealm; + + Future _fetchRealms() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/realms/me/available'); + _realms = List.from( + resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], + ); + } catch (err) { + if (mounted) context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + void _syncState() { + if (_belongToRealm == null) return; + + _nameController.text = _belongToRealm!.alias; + _nickController.text = _belongToRealm!.name; + _descriptionController.text = _belongToRealm!.description; + } + + @override + void initState() { + super.initState(); + _fetchRealms(); + } + + @override + void dispose() { + super.dispose(); + _nameController.dispose(); + _nickController.dispose(); + _descriptionController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: Text( + 'fieldPublisherBelongToRealm'.tr(), + style: TextStyle( + color: Theme.of(context).hintColor, + ), + ), + items: [ + ...(_realms?.map( + (SnRealm item) => DropdownMenuItem( + value: item, + child: Row( + children: [ + AccountImage( + content: item.avatar, + radius: 16, + fallbackWidget: const Icon( + Symbols.group, + size: 16, + ), + ), + const Gap(12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.name).textStyle( + Theme.of(context).textTheme.bodyMedium!), + Text( + item.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).textStyle( + Theme.of(context).textTheme.bodySmall!), + ], + ), + ), + ], + ), + ), + ) ?? + []), + DropdownMenuItem( + value: null, + child: Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context).colorScheme.onSurface, + child: const Icon(Symbols.clear), + ), + const Gap(12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('fieldPublisherBelongToRealmUnset') + .tr() + .textStyle( + Theme.of(context).textTheme.bodyMedium!, + ), + ], + ), + ), + ], + ), + ), + ], + value: _belongToRealm, + onChanged: (SnRealm? value) { + _belongToRealm = value; + _syncState(); + setState(() {}); + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.only(right: 16), + height: 60, + ), + menuItemStyleData: const MenuItemStyleData( + height: 60, + ), + ), + ), + Column( + children: [ + TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'fieldUsername'.tr(), + helperText: 'fieldUsernameCannotEditHint'.tr(), + helperMaxLines: 2, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(4), + TextField( + controller: _nickController, + decoration: InputDecoration( + labelText: 'fieldNickname'.tr(), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(4), + TextField( + controller: _descriptionController, + minLines: 3, + maxLines: null, + decoration: InputDecoration( + labelText: 'fieldDescription'.tr(), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ).padding(horizontal: 8), + const Gap(16), + Card( + child: SizedBox( + width: double.infinity, + child: Row( + children: [ + AccountImage(content: _belongToRealm?.avatar, radius: 24), + const Gap(16), + Column( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(_nickController.text) + .textStyle(Theme.of(context).textTheme.titleLarge!), + const Gap(4), + Text('@${_nameController.text}') .textStyle(Theme.of(context).textTheme.bodySmall!), ], ),