Compare commits

...

7 Commits

10 changed files with 278 additions and 100 deletions

View File

@ -15,7 +15,7 @@ body:json {
"client_id": "alphabot", "client_id": "alphabot",
"client_secret": "_uR0sVnHTh", "client_secret": "_uR0sVnHTh",
"remark": "新年红包", "remark": "新年红包",
"amount": 9705, "amount": 150,
"payee_id": 2 "payee_id": 18
} }
} }

View File

@ -638,5 +638,17 @@
"pollVotes": { "pollVotes": {
"one": "{} vote", "one": "{} vote",
"other": "{} votes" "other": "{} votes"
} },
"publisherDelete": "Delete Publisher {}",
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
"channelIsPublic": "Public Channel",
"channelIsPublicDescription": "The channel is public, anyone can join.",
"channelIsCommunity": "Community Channel",
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
"realmIsPublic": "Public Realm",
"realmIsPublicDescription": "The realm is public, anyone can join.",
"realmIsCommunity": "Community Realm",
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
"realmLeave": "Leave Realm",
"realmLeaveDescription": "Leave the current realm and delete the realm's identity."
} }

View File

@ -637,5 +637,17 @@
"pollVotes": { "pollVotes": {
"one": "{} 票", "one": "{} 票",
"other": "{} 票" "other": "{} 票"
} },
"publisherDelete": "删除发布者 {}",
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
"channelIsPublic": "公开频道",
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
"channelIsCommunity": "社区频道",
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
"realmIsPublic": "公开领域",
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
"realmIsCommunity": "社区领域",
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
"realmLeave": "离开领域",
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。"
} }

View File

@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
} }
} }
Future<void> _deletePublisher(SnPublisher publisher) async {
final confirm = await context.showConfirmDialog(
'publisherDelete'.tr(args: ['#${publisher.name}']),
'publisherDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
setState(() => _isBusy = true);
try {
await context
.read<SnNetworkProvider>()
.client
.delete('/cgi/co/publishers/${publisher.name}');
if (!mounted) return;
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
_publishers.remove(publisher);
_fetchPublishers();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
}); });
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deletePublisher(publisher);
},
),
], ],
), ),
); );

View File

@ -37,6 +37,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
SnChannel? _editingChannel; SnChannel? _editingChannel;
bool _isPublic = false;
bool _isCommunity = false;
Future<void> _fetchRealms() async { Future<void> _fetchRealms() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
@ -67,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
_aliasController.text = _editingChannel!.alias; _aliasController.text = _editingChannel!.alias;
_nameController.text = _editingChannel!.name; _nameController.text = _editingChannel!.name;
_descriptionController.text = _editingChannel!.description; _descriptionController.text = _editingChannel!.description;
_isPublic = _editingChannel!.isPublic;
_isCommunity = _editingChannel!.isCommunity;
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -88,6 +93,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
: uuid.v4().replaceAll('-', '').substring(0, 12), : uuid.v4().replaceAll('-', '').substring(0, 12),
'name': _nameController.text, 'name': _nameController.text,
'description': _descriptionController.text, 'description': _descriptionController.text,
'is_public': _isPublic,
'is_community': _isCommunity,
}; };
try { try {
@ -271,6 +278,23 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile(
value: _isPublic,
title: Text('channelIsPublic'.tr()),
subtitle: Text('channelIsPublicDescription'.tr()),
onChanged: (value) {
setState(() => _isPublic = value ?? false);
},
),
CheckboxListTile(
value: _isCommunity,
title: Text('channelIsCommunity'.tr()),
subtitle: Text('channelIsCommunityDescription'.tr()),
onChanged: (value) {
setState(() => _isCommunity = value ?? false);
},
),
const Gap(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@ -91,9 +91,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
final beforeId = config.prefs.getInt('int_last_publisher_id'); final beforeId = config.prefs.getInt('int_last_publisher_id');
_writeController.setPublisher( _writeController
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? .setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
_publishers?.firstOrNull);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -138,6 +137,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
builder: (context) => _PostPublisherPopup( builder: (context) => _PostPublisherPopup(
controller: _writeController, controller: _writeController,
publishers: _publishers, publishers: _publishers,
onUpdate: () {
_fetchPublishers();
},
), ),
); );
} }
@ -162,8 +164,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey); hotKeyManager.unregister(_pasteHotKey);
}
super.dispose(); super.dispose();
} }
@ -187,8 +190,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
if (widget.extraProps != null) { if (widget.extraProps != null) {
_writeController.contentController.text = widget.extraProps!.text ?? ''; _writeController.contentController.text = widget.extraProps!.text ?? '';
_writeController.titleController.text = widget.extraProps!.title ?? ''; _writeController.titleController.text = widget.extraProps!.title ?? '';
_writeController.descriptionController.text = _writeController.descriptionController.text = widget.extraProps!.description ?? '';
widget.extraProps!.description ?? '';
_writeController.addAttachments(widget.extraProps!.attachments ?? []); _writeController.addAttachments(widget.extraProps!.attachments ?? []);
} }
} }
@ -209,9 +211,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _writeController.title.isNotEmpty text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
? _writeController.title
: 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
@ -238,8 +238,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
if (_writeController.editingPost != null) if (_writeController.editingPost != null)
Container( Container(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
@ -253,9 +252,63 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
const Icon(Icons.edit, size: 16), const Icon(Icons.edit, size: 16),
const Gap(10), const Gap(10),
Text('postEditingNotice').tr(args: [ Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
'@${_writeController.editingPost!.publisher.name}' ],
]), ),
),
if (_writeController.replyingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 16),
const Gap(10),
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.replyingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (_writeController.repostingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.forward, size: 16),
const Gap(10),
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.repostingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
], ],
), ),
), ),
@ -285,8 +338,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}) })
.padding(top: 8), .padding(top: 8),
), ),
if (_writeController.attachments.isNotEmpty || if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
_writeController.thumbnail != null)
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -296,8 +348,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
attachments: _writeController.attachments, attachments: _writeController.attachments,
isBusy: _writeController.isBusy, isBusy: _writeController.isBusy,
onUpload: (int idx) async { onUpload: (int idx) async {
await _writeController.uploadSingleAttachment( await _writeController.uploadSingleAttachment(context, idx);
context, idx);
}, },
onPostSetThumbnail: (int? idx) { onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx); _writeController.setThumbnail(idx);
@ -306,12 +357,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_writeController.contentController.text += _writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})'; '\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
}, },
onUpdate: onUpdate: (int idx, PostWriteMedia updatedMedia) async {
(int idx, PostWriteMedia updatedMedia) async {
_writeController.setIsBusy(true); _writeController.setIsBusy(true);
try { try {
_writeController.setAttachmentAt( _writeController.setAttachmentAt(idx, updatedMedia);
idx, updatedMedia);
} finally { } finally {
_writeController.setIsBusy(false); _writeController.setIsBusy(false);
} }
@ -324,8 +373,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_writeController.setIsBusy(false); _writeController.setIsBusy(false);
} }
}, },
onUpdateBusy: (state) => onUpdateBusy: (state) => _writeController.setIsBusy(state),
_writeController.setIsBusy(state),
).padding(bottom: 8), ).padding(bottom: 8),
), ),
], ],
@ -336,13 +384,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_writeController.isBusy && if (_writeController.isBusy && _writeController.progress != null)
_writeController.progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress), tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_writeController.isBusy) else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
@ -351,14 +397,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
Container( Container(
child: _writeController.temporaryRestored child: _writeController.temporaryRestored
? Container( ? Container(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1 / width: 1 / MediaQuery.of(context).devicePixelRatio,
MediaQuery.of(context).devicePixelRatio,
), ),
), ),
), ),
@ -367,9 +411,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
const Icon(Icons.restore, size: 20), const Icon(Icons.restore, size: 20),
const Gap(8), const Gap(8),
Expanded( Expanded(child: Text('postLocalDraftRestored').tr()),
child:
Text('postLocalDraftRestored').tr()),
InkWell( InkWell(
child: Text('dialogDismiss').tr(), child: Text('dialogDismiss').tr(),
onTap: () { onTap: () {
@ -380,10 +422,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
)) ))
: const SizedBox.shrink(), : const SizedBox.shrink(),
) )
.height(_writeController.temporaryRestored ? 32 : 0, .height(_writeController.temporaryRestored ? 32 : 0, animate: true)
animate: true) .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
.animate(const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -403,18 +443,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
if (_writeController.mode == 'stories') if (_writeController.mode == 'stories')
IconButton( IconButton(
icon: Icon(Symbols.poll, icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
color: Theme.of(context)
.colorScheme
.primary),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor: _writeController.poll == null
_writeController.poll == null ? null
? null : WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
: WidgetStatePropertyAll(
Theme.of(context)
.colorScheme
.surfaceContainer),
), ),
onPressed: () { onPressed: () {
_showPollEditorDialog(); _showPollEditorDialog();
@ -426,8 +459,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: (_writeController.isBusy || onPressed: (_writeController.isBusy || _writeController.publisher == null)
_writeController.publisher == null)
? null ? null
: () { : () {
_writeController.sendPost(context).then((_) { _writeController.sendPost(context).then((_) {
@ -464,8 +496,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
class _PostPublisherPopup extends StatelessWidget { class _PostPublisherPopup extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final List<SnPublisher>? publishers; final List<SnPublisher>? publishers;
final Function onUpdate;
const _PostPublisherPopup({required this.controller, this.publishers}); const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -477,9 +510,7 @@ class _PostPublisherPopup extends StatelessWidget {
children: [ children: [
const Icon(Symbols.face, size: 24), const Icon(Symbols.face, size: 24),
const Gap(16), const Gap(16),
Text('accountPublishers', Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
style: Theme.of(context).textTheme.titleLarge)
.tr(),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
ListTile( ListTile(
@ -488,7 +519,11 @@ class _PostPublisherPopup extends StatelessWidget {
subtitle: Text('publisherNewSubtitle').tr(), subtitle: Text('publisherNewSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew'); GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
if (value == true) {
onUpdate();
}
});
}, },
), ),
const Divider(height: 1), const Divider(height: 1),
@ -551,8 +586,7 @@ class _PostStoryEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@ -567,8 +601,7 @@ class _PostStoryEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@ -658,8 +691,7 @@ class _PostArticleEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -738,8 +770,7 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@ -750,8 +781,7 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
isCollapsed: true, isCollapsed: true,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@ -766,8 +796,7 @@ class _PostQuestionEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@ -803,8 +832,7 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachment?>( final result = await showDialog<SnAttachment?>(
context: context, context: context,
builder: (context) => PendingAttachmentAltDialog( builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
media: PostWriteMedia(controller.videoAttachment)),
); );
if (result == null) return; if (result == null) return;
@ -816,8 +844,7 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachmentBoost?>( final result = await showDialog<SnAttachmentBoost?>(
context: context, context: context,
builder: (context) => PendingAttachmentBoostDialog( builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
media: PostWriteMedia(controller.videoAttachment)),
); );
if (result == null) return; if (result == null) return;
@ -860,8 +887,7 @@ class _PostVideoEditor extends StatelessWidget {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
controller.setVideoAttachment(null); controller.setVideoAttachment(null);
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
@ -953,8 +979,7 @@ class _PostVideoEditor extends StatelessWidget {
label: 'attachmentCopyRandomId'.tr(), label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy, icon: Symbols.content_copy,
onSelected: () { onSelected: () {
Clipboard.setData( Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
ClipboardData(text: controller.videoAttachment!.rid));
}, },
), ),
MenuItem( MenuItem(
@ -973,9 +998,7 @@ class _PostVideoEditor extends StatelessWidget {
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: controller.videoAttachment == null onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
? () => _selectVideo(context)
: null,
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: controller.videoAttachment == null child: controller.videoAttachment == null

View File

@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
_aliasController.text = out.alias; _aliasController.text = out.alias;
_nameController.text = out.name; _nameController.text = out.name;
_descriptionController.text = out.description; _descriptionController.text = out.description;
_isPublic = out.isPublic;
_isCommunity = out.isCommunity;
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
bool _isPublic = false;
bool _isCommunity = false;
Future<void> _updateImage(String place) async { Future<void> _updateImage(String place) async {
final image = await _imagePicker.pickImage(source: ImageSource.gallery); final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return; if (image == null) return;
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
'description': _descriptionController.text, 'description': _descriptionController.text,
'avatar': _avatar, 'avatar': _avatar,
'banner': _banner, 'banner': _banner,
'is_public': _isPublic,
'is_community': _isCommunity,
}; };
try { try {
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile(
value: _isPublic,
title: Text('realmIsPublic'.tr()),
subtitle: Text('realmIsPublicDescription'.tr()),
onChanged: (value) {
setState(() => _isPublic = value ?? false);
},
),
CheckboxListTile(
value: _isCommunity,
title: Text('realmIsCommunity'.tr()),
subtitle: Text('realmIsCommunityDescription'.tr()),
onChanged: (value) {
setState(() => _isCommunity = value ?? false);
},
),
const Gap(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@ -357,6 +357,28 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
} }
} }
Future<void> _leaveRealm() async {
final confirm = await context.showConfirmDialog(
'realmLeave'.tr(),
'realmLeaveDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me');
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
@ -367,22 +389,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
children: [ children: [
const Gap(8), const Gap(8),
ListTile( ListTile(
leading: const Icon(Symbols.edit), leading: const Icon(Symbols.logout),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
title: Text('realmEdit').tr(), title: Text('realmLeave').tr(),
subtitle: Text('realmEditDescription').tr(), subtitle: Text('realmLeaveDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () { onTap: _isBusy ? null : () => _leaveRealm(),
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': widget.realm!.alias},
).then((value) {
if (value != null) {
widget.onUpdate();
}
});
},
), ),
if (isOwned)
ListTile(
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),
title: Text('realmEdit').tr(),
subtitle: Text('realmEditDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': widget.realm!.alias},
).then((value) {
if (value != null) {
widget.onUpdate();
}
});
},
),
if (isOwned) if (isOwned)
ListTile( ListTile(
leading: const Icon(Symbols.delete), leading: const Icon(Symbols.delete),

View File

@ -48,6 +48,11 @@ class ChatMessageInputState extends State<ChatMessageInput> {
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
final HotKey _newLineHotKey = HotKey(
key: PhysicalKeyboardKey.enter,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp,
);
void _registerHotKey() { void _registerHotKey() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
@ -61,6 +66,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
)); ));
setState(() {}); setState(() {});
}); });
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
if (_contentController.text.isEmpty) return;
_contentController.text += '\n';
});
} }
@override @override
@ -205,7 +214,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
_contentController.dispose(); _contentController.dispose();
_focusNode.dispose(); _focusNode.dispose();
_dismissEmojiPicker(); _dismissEmojiPicker();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey);
hotKeyManager.unregister(_newLineHotKey);
}
super.dispose(); super.dispose();
} }
@ -345,6 +357,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
_sendMessage(); _sendMessage();
_focusNode.requestFocus(); _focusNode.requestFocus();
}, },
maxLines: null,
), ),
), ),
const Gap(8), const Gap(8),

View File

@ -965,7 +965,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': data.typePlural}, pathParameters: {'mode': 'stories'},
queryParameters: {'replying': data.id.toString()}, queryParameters: {'replying': data.id.toString()},
); );
}, },
@ -981,7 +981,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': data.typePlural}, pathParameters: {'mode': 'stories'},
queryParameters: {'reposting': data.id.toString()}, queryParameters: {'reposting': data.id.toString()},
); );
}, },