✨ Create attachment with referenced link
This commit is contained in:
		| @@ -120,6 +120,25 @@ class SnAttachmentProvider { | ||||
|     'webp': 'image/webp', | ||||
|   }; | ||||
|  | ||||
|   Future<SnAttachment> createWithReferenceLink( | ||||
|     String url, | ||||
|     String pool, | ||||
|     Map<String, dynamic>? metadata, { | ||||
|     String? mimetype, | ||||
|   }) async { | ||||
|     final resp = await _sn.client.post( | ||||
|       '/cgi/uc/attachments/referenced', | ||||
|       data: { | ||||
|         'url': url, | ||||
|         'pool': pool, | ||||
|         'metadata': metadata, | ||||
|         if (mimetype != null) 'mimetype': mimetype, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     return SnAttachment.fromJson(resp.data); | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> directUploadOne( | ||||
|     Uint8List data, | ||||
|     String filename, | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/gestures.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:go_router/go_router.dart'; | ||||
| import 'package:hotkey_manager/hotkey_manager.dart'; | ||||
| @@ -16,7 +15,6 @@ import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/config.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/types/attachment.dart'; | ||||
| @@ -25,8 +23,7 @@ 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'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_alt.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_actions.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| @@ -1130,77 +1127,6 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|     controller.setVideoAttachment(video); | ||||
|   } | ||||
|  | ||||
|   void _setAlt(BuildContext context) async { | ||||
|     if (controller.videoAttachment == null) return; | ||||
|  | ||||
|     final result = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentAltDialog( | ||||
|           media: PostWriteMedia(controller.videoAttachment)), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     controller.setVideoAttachment(result); | ||||
|   } | ||||
|  | ||||
|   Future<void> _createBoost(BuildContext context) async { | ||||
|     if (controller.videoAttachment == null) return; | ||||
|  | ||||
|     final result = await showDialog<SnAttachmentBoost?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentBoostDialog( | ||||
|           media: PostWriteMedia(controller.videoAttachment)), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     final newAttach = controller.videoAttachment!.copyWith( | ||||
|       boosts: [...controller.videoAttachment!.boosts, result], | ||||
|     ); | ||||
|  | ||||
|     controller.setVideoAttachment(newAttach); | ||||
|   } | ||||
|  | ||||
|   void _setThumbnail(BuildContext context) async { | ||||
|     if (controller.videoAttachment == null) return; | ||||
|  | ||||
|     final thumbnail = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => AttachmentInputDialog( | ||||
|         title: 'attachmentSetThumbnail'.tr(), | ||||
|         pool: 'interactive', | ||||
|         analyzeNow: true, | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     try { | ||||
|       final attach = context.read<SnAttachmentProvider>(); | ||||
|       final newAttach = await attach.updateOne( | ||||
|         controller.videoAttachment!, | ||||
|         thumbnailId: thumbnail.id, | ||||
|       ); | ||||
|       controller.setVideoAttachment(newAttach); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment(BuildContext context) async { | ||||
|     if (controller.videoAttachment == null) return; | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client | ||||
|           .delete('/cgi/uc/attachments/${controller.videoAttachment!.id}'); | ||||
|       controller.setVideoAttachment(null); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
| @@ -1274,80 +1200,49 @@ class _PostVideoEditor extends StatelessWidget { | ||||
|                     borderRadius: BorderRadius.circular(16), | ||||
|                     border: Border.all(color: Theme.of(context).dividerColor), | ||||
|                   ), | ||||
|                   child: ContextMenuRegion( | ||||
|                     contextMenu: ContextMenu( | ||||
|                       entries: [ | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentSetAlt'.tr(), | ||||
|                           icon: Symbols.description, | ||||
|                           onSelected: () { | ||||
|                             _setAlt(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentBoost'.tr(), | ||||
|                           icon: Symbols.bolt, | ||||
|                           onSelected: () { | ||||
|                             _createBoost(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentSetThumbnail'.tr(), | ||||
|                           icon: Symbols.image, | ||||
|                           onSelected: () { | ||||
|                             _setThumbnail(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'attachmentCopyRandomId'.tr(), | ||||
|                           icon: Symbols.content_copy, | ||||
|                           onSelected: () { | ||||
|                             Clipboard.setData(ClipboardData( | ||||
|                                 text: controller.videoAttachment!.rid)); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'delete'.tr(), | ||||
|                           icon: Symbols.delete, | ||||
|                           onSelected: () => _deleteAttachment(context), | ||||
|                         ), | ||||
|                         MenuItem( | ||||
|                           label: 'unlink'.tr(), | ||||
|                           icon: Symbols.link_off, | ||||
|                           onSelected: () { | ||||
|                             controller.setVideoAttachment(null); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     child: InkWell( | ||||
|                       borderRadius: BorderRadius.circular(16), | ||||
|                       onTap: controller.videoAttachment == null | ||||
|                           ? () => _selectVideo(context) | ||||
|                           : null, | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: controller.videoAttachment == null | ||||
|                             ? Center( | ||||
|                                 child: Row( | ||||
|                                   mainAxisSize: MainAxisSize.min, | ||||
|                                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                   children: [ | ||||
|                                     const Icon(Icons.add), | ||||
|                                     const Gap(4), | ||||
|                                     Text('postVideoUpload'.tr()), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ) | ||||
|                             : ClipRRect( | ||||
|                                 borderRadius: BorderRadius.circular(16), | ||||
|                                 child: AttachmentItem( | ||||
|                                   data: controller.videoAttachment!, | ||||
|                                   heroTag: const Uuid().v4(), | ||||
|                   child: InkWell( | ||||
|                     borderRadius: BorderRadius.circular(16), | ||||
|                     onTap: controller.videoAttachment == null | ||||
|                         ? () => _selectVideo(context) | ||||
|                         : () { | ||||
|                             showModalBottomSheet( | ||||
|                               context: context, | ||||
|                               builder: (context) => | ||||
|                                   PendingAttachmentActionSheet( | ||||
|                                 media: PostWriteMedia( | ||||
|                                   controller.videoAttachment!, | ||||
|                                 ), | ||||
|                               ), | ||||
|                       ), | ||||
|                             ).then((value) async { | ||||
|                               if (value is PostWriteMedia) { | ||||
|                                 controller.setVideoAttachment(value.attachment); | ||||
|                               } else if (value == false) { | ||||
|                                 controller.setVideoAttachment(null); | ||||
|                               } | ||||
|                             }); | ||||
|                           }, | ||||
|                     child: AspectRatio( | ||||
|                       aspectRatio: 16 / 9, | ||||
|                       child: controller.videoAttachment == null | ||||
|                           ? Center( | ||||
|                               child: Row( | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                   const Icon(Icons.add), | ||||
|                                   const Gap(4), | ||||
|                                   Text('postVideoUpload'.tr()), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ) | ||||
|                           : ClipRRect( | ||||
|                               borderRadius: BorderRadius.circular(16), | ||||
|                               child: AttachmentItem( | ||||
|                                 data: controller.videoAttachment!, | ||||
|                                 heroTag: const Uuid().v4(), | ||||
|                               ), | ||||
|                             ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|   | ||||
| @@ -12,6 +12,9 @@ import 'package:surface/widgets/dialog.dart'; | ||||
| class AttachmentInputDialog extends StatefulWidget { | ||||
|   final String? title; | ||||
|   final bool? analyzeNow; | ||||
|   final bool canPickMedia; | ||||
|   final bool canReferenceLink; | ||||
|   final bool canRandomId; | ||||
|   final SnMediaType? mediaType; | ||||
|   final String pool; | ||||
|  | ||||
| @@ -21,6 +24,9 @@ class AttachmentInputDialog extends StatefulWidget { | ||||
|     required this.pool, | ||||
|     this.analyzeNow = false, | ||||
|     this.mediaType = SnMediaType.image, | ||||
|     this.canPickMedia = true, | ||||
|     this.canReferenceLink = true, | ||||
|     this.canRandomId = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -29,6 +35,8 @@ class AttachmentInputDialog extends StatefulWidget { | ||||
|  | ||||
| class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|   final _randomIdController = TextEditingController(); | ||||
|   final _referenceLinkController = TextEditingController(); | ||||
|   final _referenceMimetypeController = TextEditingController(); | ||||
|  | ||||
|   XFile? _file; | ||||
|   double? _progress; | ||||
| @@ -61,6 +69,22 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } else if (_referenceLinkController.text.isNotEmpty) { | ||||
|       try { | ||||
|         final attachment = await attach.createWithReferenceLink( | ||||
|           _referenceLinkController.text, | ||||
|           widget.pool, | ||||
|           null, | ||||
|           mimetype: _referenceMimetypeController.text.isNotEmpty | ||||
|               ? _referenceMimetypeController.text | ||||
|               : null, | ||||
|         ); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } else if (_file != null) { | ||||
|       try { | ||||
|         final place = await attach.chunkedUploadInitialize( | ||||
| @@ -90,44 +114,98 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|     return AlertDialog( | ||||
|       title: Text(widget.title ?? 'attachmentInputDialog'.tr()), | ||||
|       content: Column( | ||||
|         spacing: 16, | ||||
|         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 UnderlineInputBorder(), | ||||
|               isDense: true, | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(24), | ||||
|           Text('attachmentInputNew').tr().fontSize(14), | ||||
|           Card( | ||||
|             child: Column( | ||||
|           if (_file == null && | ||||
|               _referenceLinkController.text.isEmpty && | ||||
|               widget.canRandomId) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 ListTile( | ||||
|                   shape: RoundedRectangleBorder( | ||||
|                     borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 Text('attachmentInputUseRandomId').tr().fontSize(14), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
|                   controller: _randomIdController, | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     isDense: true, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           if (_file == null && | ||||
|               _referenceLinkController.text.isEmpty && | ||||
|               widget.canReferenceLink) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('attachmentReferenceLink').tr().fontSize(14), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
|                   controller: _referenceLinkController, | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'fieldAttachmentReferenceLink'.tr(), | ||||
|                     helperText: 'attachmentReferenceLinkDescription'.tr(), | ||||
|                     helperMaxLines: 3, | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     isDense: true, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|                 const Gap(8), | ||||
|                 TextField( | ||||
|                   controller: _referenceLinkController, | ||||
|                   decoration: InputDecoration( | ||||
|                     labelText: 'fieldAttachmentMimetype'.tr(), | ||||
|                     helperText: 'class/type', | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     isDense: true, | ||||
|                   ), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           if (_referenceLinkController.text.isEmpty && | ||||
|               _randomIdController.text.isEmpty && | ||||
|               widget.canPickMedia) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('attachmentInputNew').tr().fontSize(14), | ||||
|                 Card( | ||||
|                   margin: EdgeInsets.only(top: 8), | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       ListTile( | ||||
|                         shape: RoundedRectangleBorder( | ||||
|                           borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                         ), | ||||
|                         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: _file == null | ||||
|                             ? Text('unset').tr() | ||||
|                             : Text('waitingForUpload').tr(), | ||||
|                         onTap: () { | ||||
|                           _pickMedia(); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   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: _file == null | ||||
|                       ? Text('unset').tr() | ||||
|                       : Text('waitingForUpload').tr(), | ||||
|                   onTap: () { | ||||
|                     _pickMedia(); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           if (_isBusy) | ||||
|             LinearProgressIndicator( | ||||
|               value: _progress, | ||||
|   | ||||
| @@ -27,7 +27,12 @@ import 'package:surface/widgets/loading_indicator.dart'; | ||||
|  | ||||
| class PendingAttachmentActionSheet extends StatefulWidget { | ||||
|   final PostWriteMedia media; | ||||
|   const PendingAttachmentActionSheet({super.key, required this.media}); | ||||
|   final bool canInsertLink; | ||||
|   const PendingAttachmentActionSheet({ | ||||
|     super.key, | ||||
|     required this.media, | ||||
|     this.canInsertLink = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<PendingAttachmentActionSheet> createState() => | ||||
| @@ -270,15 +275,16 @@ class _PendingAttachmentActionSheetState | ||||
|                     Navigator.pop(context); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.add_link), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentInsertLink').tr(), | ||||
|                   onTap: () { | ||||
|                     Navigator.pop(context, 'link'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 if (widget.canInsertLink) | ||||
|                   ListTile( | ||||
|                     minTileHeight: 48, | ||||
|                     leading: const Icon(Symbols.add_link), | ||||
|                     contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                     title: Text('attachmentInsertLink').tr(), | ||||
|                     onTap: () { | ||||
|                       Navigator.pop(context, 'link'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.bolt), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user