diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 7b67205..b2c7ee0 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -671,5 +671,8 @@ "attachmentBillingDiscount": "Free space", "attachmentBillingRatio": "Usage", "attachmentBillingHint": "Sliding Window Pricing®\nFees will only apply if the size of the file uploaded within 24 hours exceeds the free space.", - "postThumbnail": "Post Thumbnail" + "postThumbnail": "Post Thumbnail", + "accountRealms": "Realms", + "postInGlobal": "Global", + "postInGlobalDescription": "Do not link this post with any realm." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1e8f810..ce0e30b 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -669,5 +669,8 @@ "attachmentBillingUploaded": "已占用的字节数", "attachmentBillingDiscount": "免费的字节数", "attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。", - "postThumbnail": "帖子缩略图" + "postThumbnail": "帖子缩略图", + "accountRealms": "领域", + "postInGlobal": "全站", + "postInGlobalDescription": "不关联此帖子与任何领域。" } diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index d55f25a..64da090 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/types/poll.dart'; import 'package:surface/types/post.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:video_compress/video_compress.dart'; @@ -197,6 +198,7 @@ class PostWriteController extends ChangeNotifier { bool isLoading = false, isBusy = false; double? progress; + SnRealm? realm; SnPublisher? publisher; SnPost? editingPost, repostingPost, replyingPost; @@ -625,6 +627,11 @@ class PostWriteController extends ChangeNotifier { notifyListeners(); } + void setRealm(SnRealm? value) { + realm = value; + notifyListeners(); + } + void setProgress(double? value) { progress = value; _temporaryPlanSave(); diff --git a/lib/main.dart b/lib/main.dart index a3f0396..e359e43 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,7 @@ import 'package:surface/providers/post.dart'; import 'package:surface/providers/relationship.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/theme.dart'; @@ -159,6 +160,7 @@ class SolianApp extends StatelessWidget { Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnStickerProvider(ctx)), + Provider(create: (ctx) => SnRealmProvider(ctx)), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), diff --git a/lib/providers/sn_realm.dart b/lib/providers/sn_realm.dart new file mode 100644 index 0000000..09e438a --- /dev/null +++ b/lib/providers/sn_realm.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/realm.dart'; + +class SnRealmProvider { + late final SnNetworkProvider _sn; + + SnRealmProvider(BuildContext context) { + _sn = context.read(); + } + + Future> listAvailableRealms() async { + final resp = await _sn.client.get('/cgi/id/realms/me/available'); + final out = List.from( + resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], + ); + return out; + } + + Future getRealm(String alias) async { + final resp = await _sn.client.get('/cgi/id/realms/$alias'); + final out = SnRealm.fromJson(resp.data); + return out; + } +} diff --git a/lib/screens/chat/channel_detail.dart b/lib/screens/chat/channel_detail.dart index 2bb5aa5..2a57abf 100644 --- a/lib/screens/chat/channel_detail.dart +++ b/lib/screens/chat/channel_detail.dart @@ -104,7 +104,7 @@ class _ChannelDetailScreenState extends State { try { final sn = context.read(); await sn.client.delete( - '/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/members/me', + '/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me', ); if (!mounted) return; Navigator.pop(context, false); diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 08d5698..6b17f88 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -20,6 +20,7 @@ import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/types/post.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; @@ -35,6 +36,8 @@ import 'package:provider/provider.dart'; import 'package:surface/widgets/post/post_poll_editor.dart'; import 'package:uuid/uuid.dart'; +import '../../providers/sn_realm.dart'; + class PostEditorExtra { final String? text; final String? title; @@ -79,6 +82,7 @@ class _PostEditorScreenState extends State { bool get _isLoading => _isFetching || _writeController.isLoading; List? _publishers; + List? _realms; Future _fetchPublishers() async { setState(() => _isFetching = true); @@ -101,6 +105,16 @@ class _PostEditorScreenState extends State { } } + Future _fetchRealms() async { + final rels = context.read(); + try { + _realms = await rels.listAvailableRealms(); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } + } + void _updateMeta() { showModalBottomSheet( context: context, @@ -144,6 +158,19 @@ class _PostEditorScreenState extends State { ); } + void _showRealmPopup() { + showModalBottomSheet( + context: context, + builder: (context) => _PostRealmPopup( + controller: _writeController, + realms: _realms, + onUpdate: () { + _fetchRealms(); + }, + ), + ); + } + void _showPollEditorDialog() async { final poll = await showDialog( context: context, @@ -194,6 +221,7 @@ class _PostEditorScreenState extends State { } else { _writeController.setMode(widget.mode); } + _fetchRealms(); _fetchPublishers(); _writeController.fetchRelatedPost( context, @@ -335,6 +363,7 @@ class _PostEditorScreenState extends State { 'stories' => _PostStoryEditor( controller: _writeController, onTapPublisher: _showPublisherPopup, + onTapRealm: _showRealmPopup, ), 'articles' => _PostArticleEditor( controller: _writeController, @@ -575,11 +604,65 @@ class _PostPublisherPopup extends StatelessWidget { } } +class _PostRealmPopup extends StatelessWidget { + final PostWriteController controller; + final List? realms; + final Function onUpdate; + + const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.face, size: 24), + const Gap(16), + Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + ListTile( + leading: const Icon(Symbols.close), + title: Text('postInGlobal').tr(), + subtitle: Text('postInGlobalDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + onTap: () { + controller.setRealm(null); + Navigator.pop(context, true); + }, + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: realms?.length ?? 0, + itemBuilder: (context, idx) { + final realm = realms![idx]; + return ListTile( + title: Text(realm.name), + subtitle: Text('@${realm.alias}'), + leading: AccountImage(content: realm.avatar, radius: 18), + onTap: () { + controller.setRealm(realm); + Navigator.pop(context, true); + }, + ); + }, + ), + ), + ], + ); + } +} + class _PostStoryEditor extends StatelessWidget { final PostWriteController controller; final Function? onTapPublisher; + final Function? onTapRealm; - const _PostStoryEditor({required this.controller, this.onTapPublisher}); + const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); @override Widget build(BuildContext context) { @@ -589,17 +672,36 @@ class _PostStoryEditor extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Material( - elevation: 2, - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: GestureDetector( - onTap: () { - onTapPublisher?.call(); - }, - child: AccountImage( - content: controller.publisher?.avatar, + Column( + children: [ + Material( + elevation: 2, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapPublisher?.call(); + }, + child: AccountImage( + content: controller.publisher?.avatar, + ), + ), ), - ), + const Gap(10), + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapRealm?.call(); + }, + child: AccountImage( + content: controller.realm?.avatar, + fallbackWidget: const Icon(Symbols.globe, size: 20), + radius: 14, + ), + ), + ), + ], ), Expanded( child: Column( diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index 14ba244..b8f5256 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -365,7 +365,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { final sn = context.read(); try { - await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me'); + await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/me'); if (!mounted) return; Navigator.pop(context, true); } catch (err) {