Compare commits
	
		
			3 Commits
		
	
	
		
			4d96a15c31
			...
			619c90cdd9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 619c90cdd9 | |||
| 168d51c9fe | |||
| d4b831f98e | 
							
								
								
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| meta { | ||||
|   name: Developer Notify All Users | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/id/dev/notify/all | ||||
|   body: json | ||||
|   auth: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "{{third_client_id}}", | ||||
|     "client_secret":"{{third_client_tk}}", | ||||
|     "type": "general", | ||||
|     "subject": "Merry Christmas!", | ||||
|     "subtitle": "一条来自 Solar Network 团队的信息", | ||||
|     "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", | ||||
|     "metadata": { | ||||
|       "image": "6EqsYQwmFRCkbmhR" | ||||
|     }, | ||||
|     "priority": 10 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "version": "1", | ||||
|   "name": "Solar Network", | ||||
|   "type": "collection", | ||||
|   "ignore": [ | ||||
|     "node_modules", | ||||
|     ".git" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| vars { | ||||
|   endpoint: https://api.sn.solsynth.dev | ||||
|   third_client_id: alphabot | ||||
| } | ||||
| vars:secret [ | ||||
|   atk, | ||||
|   third_client_tk | ||||
| ] | ||||
| @@ -281,16 +281,22 @@ | ||||
|     "one": "{} attachment", | ||||
|     "other": "{} attachments" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "Random ID", | ||||
|   "addAttachmentFromAlbum": "Add from album", | ||||
|   "addAttachmentFromClipboard": "Paste file", | ||||
|   "addAttachmentFromCameraPhoto": "Take photo", | ||||
|   "addAttachmentFromCameraVideo": "Take video", | ||||
|   "addAttachmentFromRandomId": "Link via RID", | ||||
|   "attachmentPastedImage": "Pasted Image", | ||||
|   "attachmentInsertLink": "Insert Link", | ||||
|   "attachmentSetAsPostThumbnail": "Set as post thumbnail", | ||||
|   "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", | ||||
|   "attachmentSetThumbnail": "Set thumbnail", | ||||
|   "attachmentCopyRandomId": "Copy RID", | ||||
|   "attachmentUpload": "Upload", | ||||
|   "attachmentInputDialog": "Upload attachments", | ||||
|   "attachmentInputUseRandomId": "Use Random ID", | ||||
|   "attachmentInputNew": "New Upload", | ||||
|   "notification": "Notification", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "All notifications read", | ||||
| @@ -506,5 +512,6 @@ | ||||
|   "postCategoryKnowledge": "Knowledge", | ||||
|   "postCategoryLiterature": "Literature", | ||||
|   "postCategoryFunny": "Funny", | ||||
|   "postCategoryUncategorized": "Uncategorized" | ||||
|   "postCategoryUncategorized": "Uncategorized", | ||||
|   "waitingForUpload": "Waiting for upload" | ||||
| } | ||||
|   | ||||
| @@ -279,16 +279,22 @@ | ||||
|     "one": "{} 个附件", | ||||
|     "other": "{} 个附件" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "访问 ID", | ||||
|   "addAttachmentFromAlbum": "从相册中添加附件", | ||||
|   "addAttachmentFromClipboard": "粘贴附件", | ||||
|   "addAttachmentFromCameraPhoto": "拍摄照片", | ||||
|   "addAttachmentFromCameraVideo": "拍摄视频", | ||||
|   "addAttachmentFromRandomId": "通过访问 ID 链接", | ||||
|   "attachmentPastedImage": "粘贴的图片", | ||||
|   "attachmentInsertLink": "插入连接", | ||||
|   "attachmentSetAsPostThumbnail": "设置为帖子缩略图", | ||||
|   "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", | ||||
|   "attachmentSetThumbnail": "设置缩略图", | ||||
|   "attachmentCopyRandomId": "复制访问 ID", | ||||
|   "attachmentUpload": "上传", | ||||
|   "attachmentInputDialog": "上传附件", | ||||
|   "attachmentInputUseRandomId": "使用访问 ID", | ||||
|   "attachmentInputNew": "新上传附件", | ||||
|   "notification": "通知", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "无未读通知", | ||||
| @@ -504,5 +510,6 @@ | ||||
|   "postCategoryKnowledge": "知识", | ||||
|   "postCategoryLiterature": "文学", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分类" | ||||
|   "postCategoryUncategorized": "未分类", | ||||
|   "waitingForUpload": "等待上传" | ||||
| } | ||||
|   | ||||
| @@ -215,4 +215,18 @@ class SnAttachmentProvider { | ||||
|  | ||||
|     return place; | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> updateOne( | ||||
|     int id, | ||||
|     String alt, { | ||||
|     required Map<String, dynamic> metadata, | ||||
|     bool isMature = false, | ||||
|   }) async { | ||||
|     final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: { | ||||
|       'alt': alt, | ||||
|       'metadata': metadata, | ||||
|       'is_mature': isMature, | ||||
|     }); | ||||
|     return SnAttachment.fromJson(resp.data); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -31,9 +31,10 @@ class UserProvider extends ChangeNotifier { | ||||
|     final value = _config.prefs.getString(kAtkStoreKey); | ||||
|     isAuthorized = value != null; | ||||
|     notifyListeners(); | ||||
|     refreshUser().then((value) { | ||||
|     refreshUser().then((value) async { | ||||
|       if (value != null) { | ||||
|         log('Logged in as @${value.name}'); | ||||
|         log('Atk: ${await atk}'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -96,38 +96,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _writeController.addAttachments( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _writeController.dispose(); | ||||
| @@ -435,63 +403,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                               scrollDirection: Axis.vertical, | ||||
|                               child: Row( | ||||
|                                 children: [ | ||||
|                                   PopupMenuButton( | ||||
|                                     icon: Icon( | ||||
|                                       Symbols.add_photo_alternate, | ||||
|                                       color: Theme.of(context).colorScheme.primary, | ||||
|                                     ), | ||||
|                                     itemBuilder: (context) => [ | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.photo_camera), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(false); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.videocam), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraVideo').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(true); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.photo_library), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromAlbum').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _selectMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.content_paste), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromClipboard').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _pasteMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   AddPostMediaButton( | ||||
|                                     onAdd: (items) { | ||||
|                                       setState(() { | ||||
|                                         _writeController.addAttachments(items); | ||||
|                                       }); | ||||
|                                     }, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|   | ||||
							
								
								
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.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_attachment.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
|  | ||||
| class AttachmentInputDialog extends StatefulWidget { | ||||
|   final String? title; | ||||
|  | ||||
|   const AttachmentInputDialog({super.key, required this.title}); | ||||
|  | ||||
|   @override | ||||
|   State<AttachmentInputDialog> createState() => _AttachmentInputDialogState(); | ||||
| } | ||||
|  | ||||
| class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|   final _randomIdController = TextEditingController(); | ||||
|  | ||||
|   XFile? _thumbnailFile; | ||||
|  | ||||
|   void _pickImage() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickImage(source: ImageSource.gallery); | ||||
|     if (result == null) return; | ||||
|     setState(() => _thumbnailFile = result); | ||||
|   } | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   void _finishUp() async { | ||||
|     if (_isBusy) return; | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     if (_randomIdController.text.isNotEmpty) { | ||||
|       try { | ||||
|         final attachment = await attach.getOne(_randomIdController.text); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } else if (_thumbnailFile != null) { | ||||
|       try { | ||||
|         final attachment = await attach.directUploadOne( | ||||
|           (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(), | ||||
|           _thumbnailFile!.path, | ||||
|           'interactive', | ||||
|           null, | ||||
|         ); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text(widget.title ?? 'attachmentInputDialog').tr(), | ||||
|       content: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           Text('attachmentInputUseRandomId').tr().fontSize(14), | ||||
|           const Gap(8), | ||||
|           TextField( | ||||
|             controller: _randomIdController, | ||||
|             decoration: InputDecoration( | ||||
|               labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|               border: const OutlineInputBorder(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(24), | ||||
|           Text('attachmentInputNew').tr().fontSize(14), | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               leading: const Icon(Symbols.add_photo_alternate), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               title: Text('addAttachmentFromAlbum').tr(), | ||||
|               subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(), | ||||
|               onTap: () { | ||||
|                 _pickImage(); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           child: Text('dialogDismiss').tr(), | ||||
|           onPressed: _isBusy ? null : () { | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => _finishUp(), | ||||
|           child: Text('dialogConfirm').tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -123,40 +123,6 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   } | ||||
|  | ||||
|   final List<PostWriteMedia> _attachments = List.empty(growable: true); | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _attachments.addAll( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
| @@ -294,63 +260,12 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               PopupMenuButton( | ||||
|                 icon: Icon( | ||||
|                   Symbols.add_photo_alternate, | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                 ), | ||||
|                 itemBuilder: (context) => [ | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.photo_camera), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(false); | ||||
|                       }, | ||||
|                     ), | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.videocam), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraVideo').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(true); | ||||
|                       }, | ||||
|                     ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.photo_library), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromAlbum').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _selectMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.content_paste), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromClipboard').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _pasteMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               AddPostMediaButton( | ||||
|                 onAdd: (items) { | ||||
|                   setState(() { | ||||
|                     _attachments.addAll(items); | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: _isBusy ? null : _sendMessage, | ||||
|   | ||||
| @@ -6,16 +6,23 @@ import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_input.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_zoom.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class PostMediaPendingList extends StatelessWidget { | ||||
|   final PostWriteMedia? thumbnail; | ||||
| @@ -71,6 +78,32 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _setThumbnail(BuildContext context, int idx) async { | ||||
|     if (idx == -1) { | ||||
|       // Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail. | ||||
|       return; | ||||
|     } else if (attachments[idx].attachment == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final thumbnail = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => AttachmentInputDialog( | ||||
|         title: 'attachmentSetThumbnail'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final newAttach = await attach.updateOne(attachments[idx].attachment!.id, thumbnail.alt, metadata: { | ||||
|       ...attachments[idx].attachment!.metadata, | ||||
|       'thumbnail': thumbnail.rid, | ||||
|     }); | ||||
|  | ||||
|     onUpdate!(idx, PostWriteMedia(newAttach)); | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment(BuildContext context, int idx) async { | ||||
|     final media = idx == -1 ? thumbnail! : attachments[idx]; | ||||
|     if (media.attachment == null) return; | ||||
| @@ -91,6 +124,14 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|   ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) { | ||||
|     return ContextMenu( | ||||
|       entries: [ | ||||
|         if (media.attachment != null && media.type == PostWriteMediaType.video) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetThumbnail'.tr(), | ||||
|             icon: Symbols.image, | ||||
|             onSelected: () { | ||||
|               _setThumbnail(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment == null && onUpload != null) | ||||
|           MenuItem( | ||||
|               label: 'attachmentUpload'.tr(), | ||||
| @@ -98,7 +139,10 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onSelected: () { | ||||
|                 onUpload!(idx); | ||||
|               }), | ||||
|         if (media.attachment != null && onPostSetThumbnail != null && idx != -1) | ||||
|         if (media.attachment != null && | ||||
|             media.type == PostWriteMediaType.image && | ||||
|             onPostSetThumbnail != null && | ||||
|             idx != -1) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.gallery_thumbnail, | ||||
| @@ -106,7 +150,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onPostSetThumbnail!(idx); | ||||
|             }, | ||||
|           ) | ||||
|         else if (media.attachment != null && onPostSetThumbnail != null) | ||||
|         else if (media.attachment != null && media.type == PostWriteMediaType.image && onPostSetThumbnail != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentUnsetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.cancel, | ||||
| @@ -139,6 +183,14 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|             icon: Symbols.crop, | ||||
|             onSelected: () => _cropImage(context, idx), | ||||
|           ), | ||||
|         if (media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentCopyRandomId'.tr(), | ||||
|             icon: Symbols.content_copy, | ||||
|             onSelected: () { | ||||
|               Clipboard.setData(ClipboardData(text: media.attachment!.rid)); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null && onRemove != null) | ||||
|           MenuItem( | ||||
|             label: 'delete'.tr(), | ||||
| @@ -169,6 +221,8 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       constraints: const BoxConstraints(maxHeight: 120), | ||||
|       child: Row( | ||||
| @@ -177,40 +231,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|           if (thumbnail != null) | ||||
|             ContextMenuArea( | ||||
|               contextMenu: _createContextMenu(context, -1, thumbnail!), | ||||
|               child: Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border.all( | ||||
|                     color: Theme.of(context).dividerColor, | ||||
|                     width: 1, | ||||
|                   ), | ||||
|                   borderRadius: BorderRadius.circular(8), | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: 1, | ||||
|                     child: switch (thumbnail!.type) { | ||||
|                       PostWriteMediaType.image => Container( | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                         child: LayoutBuilder(builder: (context, constraints) { | ||||
|                             return Image( | ||||
|                               image: thumbnail!.getImageProvider( | ||||
|                                 context, | ||||
|                                 width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                 height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                               )!, | ||||
|                               fit: BoxFit.contain, | ||||
|                             ); | ||||
|                           }), | ||||
|                       ), | ||||
|                       _ => Container( | ||||
|                           color: Theme.of(context).colorScheme.surface, | ||||
|                           child: const Icon(Symbols.docs).center(), | ||||
|                         ), | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               child: _PostMediaPendingItem(media: thumbnail!), | ||||
|             ), | ||||
|           if (thumbnail != null) | ||||
|             const VerticalDivider(width: 1, thickness: 1).padding( | ||||
| @@ -227,40 +248,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|                 final media = attachments[idx]; | ||||
|                 return ContextMenuArea( | ||||
|                   contextMenu: _createContextMenu(context, idx, media), | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       border: Border.all( | ||||
|                         color: Theme.of(context).dividerColor, | ||||
|                         width: 1, | ||||
|                       ), | ||||
|                       borderRadius: BorderRadius.circular(8), | ||||
|                     ), | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 1, | ||||
|                         child: switch (media.type) { | ||||
|                           PostWriteMediaType.image => Container( | ||||
|                             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                             child: LayoutBuilder(builder: (context, constraints) { | ||||
|                                 return Image( | ||||
|                                   image: media.getImageProvider( | ||||
|                                     context, | ||||
|                                     width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                     height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                                   )!, | ||||
|                                   fit: BoxFit.contain, | ||||
|                                 ); | ||||
|                               }), | ||||
|                           ), | ||||
|                           _ => Container( | ||||
|                               color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                               child: const Icon(Symbols.docs).center(), | ||||
|                             ), | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   child: _PostMediaPendingItem(media: media), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
| @@ -270,3 +258,218 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostMediaPendingItem extends StatelessWidget { | ||||
|   final PostWriteMedia media; | ||||
|  | ||||
|   const _PostMediaPendingItem({ | ||||
|     required this.media, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       decoration: BoxDecoration( | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).dividerColor, | ||||
|           width: 1, | ||||
|         ), | ||||
|         borderRadius: BorderRadius.circular(8), | ||||
|       ), | ||||
|       child: ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: 1, | ||||
|           child: switch (media.type) { | ||||
|             PostWriteMediaType.image => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: LayoutBuilder(builder: (context, constraints) { | ||||
|                   return Image( | ||||
|                     image: media.getImageProvider( | ||||
|                       context, | ||||
|                       width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                       height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                     )!, | ||||
|                     fit: BoxFit.contain, | ||||
|                   ); | ||||
|                 }), | ||||
|               ), | ||||
|             PostWriteMediaType.video => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: media.attachment?.metadata['thumbnail'] != null | ||||
|                     ? AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment?.metadata['thumbnail'])) | ||||
|                     : const Icon(Symbols.videocam).center(), | ||||
|               ), | ||||
|             _ => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: const Icon(Symbols.docs).center(), | ||||
|               ), | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AddPostMediaButton extends StatelessWidget { | ||||
|   final Function(Iterable<PostWriteMedia>) onAdd; | ||||
|  | ||||
|   const AddPostMediaButton({super.key, required this.onAdd}); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = isVideo | ||||
|         ? await picker.pickVideo(source: ImageSource.camera) | ||||
|         : await picker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     onAdd([PostWriteMedia.fromFile(result)]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     onAdd( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     onAdd([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _linkRandomId(BuildContext context) async { | ||||
|     final randomIdController = TextEditingController(); | ||||
|     final randomId = await showDialog<String?>( | ||||
|       context: context, | ||||
|       builder: (context) => AlertDialog( | ||||
|         title: Text('addAttachmentFromRandomId').tr(), | ||||
|         content: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             TextField( | ||||
|               controller: randomIdController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|                 border: const UnderlineInputBorder(), | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           TextButton( | ||||
|             child: Text('dialogDismiss').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context); | ||||
|             }, | ||||
|           ), | ||||
|           TextButton( | ||||
|             child: Text('dialogConfirm').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context, randomIdController.text); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       randomIdController.dispose(); | ||||
|     }); | ||||
|     if (randomId == null || randomId.isEmpty) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final attachment = await attach.getOne(randomId); | ||||
|  | ||||
|     onAdd([ | ||||
|       PostWriteMedia(attachment), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return PopupMenuButton( | ||||
|       icon: Icon( | ||||
|         Symbols.add_photo_alternate, | ||||
|         color: Theme.of(context).colorScheme.primary, | ||||
|       ), | ||||
|       itemBuilder: (context) => [ | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.photo_camera), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraPhoto').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(false); | ||||
|             }, | ||||
|           ), | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.videocam), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraVideo').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(true); | ||||
|             }, | ||||
|           ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.photo_library), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromAlbum').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _selectMedia(); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.link), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromRandomId').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _linkRandomId(context); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.content_paste), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromClipboard').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _pasteMedia(); | ||||
|           }, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user