♻️ Refactored attachment meta editor
This commit is contained in:
		| @@ -63,7 +63,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|       } | ||||
|     } else if (_file != null) { | ||||
|       try { | ||||
|         final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null); | ||||
|         final place = await attach.chunkedUploadInitialize( | ||||
|             await _file!.length(), _file!.name, widget.pool, null); | ||||
|  | ||||
|         final attachment = await attach.chunkedUploadParts( | ||||
|           _file!, | ||||
| @@ -109,11 +110,17 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|             child: Column( | ||||
|               children: [ | ||||
|                 ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|                   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(), | ||||
|                   subtitle: _file == null | ||||
|                       ? Text('unset').tr() | ||||
|                       : Text('waitingForUpload').tr(), | ||||
|                   onTap: () { | ||||
|                     _pickMedia(); | ||||
|                   }, | ||||
|   | ||||
							
								
								
									
										311
									
								
								lib/widgets/attachment/pending_attachment_actions.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								lib/widgets/attachment/pending_attachment_actions.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,311 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:croppy/croppy.dart'; | ||||
| 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:gap/gap.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.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/attachment/pending_attachment_alt.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_compress.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
|  | ||||
| class PendingAttachmentActionSheet extends StatefulWidget { | ||||
|   final PostWriteMedia media; | ||||
|   const PendingAttachmentActionSheet({super.key, required this.media}); | ||||
|  | ||||
|   @override | ||||
|   State<PendingAttachmentActionSheet> createState() => | ||||
|       _PendingAttachmentActionSheetState(); | ||||
| } | ||||
|  | ||||
| class _PendingAttachmentActionSheetState | ||||
|     extends State<PendingAttachmentActionSheet> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   Future<void> _cropImage() async { | ||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|         ? await showCupertinoImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             // ignore: use_build_context_synchronously | ||||
|             imageProvider: widget.media.getImageProvider(context)!, | ||||
|           ) | ||||
|         : await showMaterialImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             // ignore: use_build_context_synchronously | ||||
|             imageProvider: widget.media.getImageProvider(context)!, | ||||
|           ); | ||||
|  | ||||
|     if (result == null) return; | ||||
|  | ||||
|     final rawBytes = | ||||
|         (await result.uiImage.toByteData(format: ImageByteFormat.png))! | ||||
|             .buffer | ||||
|             .asUint8List(); | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     final updatedMedia = PostWriteMedia.fromBytes( | ||||
|         rawBytes, widget.media.name, widget.media.type); | ||||
|     Navigator.pop(context, updatedMedia); | ||||
|   } | ||||
|  | ||||
|   Future<void> _setThumbnail() async { | ||||
|     final thumbnail = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => AttachmentInputDialog( | ||||
|         title: 'attachmentSetThumbnail'.tr(), | ||||
|         pool: 'interactive', | ||||
|         analyzeNow: true, | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     try { | ||||
|       final attach = context.read<SnAttachmentProvider>(); | ||||
|       final newAttach = await attach.updateOne(widget.media.attachment!, | ||||
|           thumbnailId: thumbnail.id); | ||||
|       if (mounted) Navigator.pop(context, newAttach); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment() async { | ||||
|     if (_isBusy) return; | ||||
|     if (widget.media.attachment == null) return; | ||||
|  | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client | ||||
|           .delete('/cgi/uc/attachments/${widget.media.attachment!.id}'); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, false); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _createBoost() async { | ||||
|     final result = await showDialog<SnAttachmentBoost?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentBoostDialog( | ||||
|         media: widget.media, | ||||
|       ), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     final newAttach = widget.media.attachment! | ||||
|         .copyWith(boosts: [...widget.media.attachment!.boosts, result]); | ||||
|     final newMedia = PostWriteMedia(newAttach); | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     Navigator.pop(context, newMedia); | ||||
|   } | ||||
|  | ||||
|   Future<void> _compressVideo() async { | ||||
|     final result = await showDialog<PostWriteMedia?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingVideoCompressDialog(media: widget.media), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     Navigator.pop(context, result); | ||||
|   } | ||||
|  | ||||
|   Future<void> _setAlt() async { | ||||
|     final result = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentAltDialog(media: widget.media), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     if (!mounted) return; | ||||
|     Navigator.pop(context, PostWriteMedia(result)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.edit, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('attachmentEditor') | ||||
|                 .tr() | ||||
|                 .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         if (widget.media.attachment == null) | ||||
|           Text('attachmentEditorUnUploadHint') | ||||
|               .tr() | ||||
|               .textStyle(Theme.of(context).textTheme.bodyMedium!) | ||||
|               .padding(horizontal: 20, bottom: 8) | ||||
|               .opacity(0.8) | ||||
|         else | ||||
|           Card( | ||||
|             margin: EdgeInsets.zero, | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text(widget.media.attachment!.alt), | ||||
|                 Row( | ||||
|                   spacing: 6, | ||||
|                   children: [ | ||||
|                     Text(widget.media.attachment!.size.formatBytes()), | ||||
|                     Text( | ||||
|                       widget.media.attachment!.mimetype, | ||||
|                       style: GoogleFonts.robotoMono(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Text('attachmentEditorUploadHint') | ||||
|                     .tr() | ||||
|                     .textStyle(Theme.of(context).textTheme.bodyMedium!) | ||||
|                     .opacity(0.8), | ||||
|               ], | ||||
|             ).padding(horizontal: 16, vertical: 8), | ||||
|           ).padding(horizontal: 16, bottom: 8), | ||||
|         LoadingIndicator(isActive: _isBusy), | ||||
|         if (widget.media.attachment == null) | ||||
|           Expanded( | ||||
|             child: ListView( | ||||
|               children: [ | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.upload), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentUpload').tr(), | ||||
|                   onTap: () => Navigator.pop(context, true), | ||||
|                 ), | ||||
|                 if (widget.media.type == SnMediaType.video) | ||||
|                   ListTile( | ||||
|                     minTileHeight: 48, | ||||
|                     leading: const Icon(Symbols.compress), | ||||
|                     title: Text('attachmentCompressVideo').tr(), | ||||
|                     onTap: () => _compressVideo(), | ||||
|                   ), | ||||
|                 if (widget.media.type == SnMediaType.image) | ||||
|                   ListTile( | ||||
|                     minTileHeight: 48, | ||||
|                     leading: const Icon(Symbols.crop), | ||||
|                     contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                     title: Text('crop').tr(), | ||||
|                     onTap: () => _cropImage(), | ||||
|                   ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.delete), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('delete').tr(), | ||||
|                   onTap: () => Navigator.pop(context, false), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ) | ||||
|         else | ||||
|           Expanded( | ||||
|             child: ListView( | ||||
|               children: [ | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.preview), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('preview').tr(), | ||||
|                   onTap: () { | ||||
|                     context.pushTransparentRoute( | ||||
|                       AttachmentZoomView(data: [widget.media.attachment!]), | ||||
|                       rootNavigator: true, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.copy_all), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentCopyRandomId').tr(), | ||||
|                   onTap: () { | ||||
|                     Clipboard.setData( | ||||
|                       ClipboardData( | ||||
|                         text: widget.media.attachment!.rid, | ||||
|                       ), | ||||
|                     ); | ||||
|                     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'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.bolt), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentBoost').tr(), | ||||
|                   onTap: () => _createBoost(), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.thumbnail_bar), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentSetThumbnail').tr(), | ||||
|                   onTap: () => _setThumbnail(), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.description), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('attachmentSetAlt').tr(), | ||||
|                   onTap: () => _setAlt(), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.link_off), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('unlink').tr(), | ||||
|                   onTap: () => Navigator.pop(context, false), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   minTileHeight: 48, | ||||
|                   leading: const Icon(Symbols.delete), | ||||
|                   contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Text('delete').tr(), | ||||
|                   onTap: () => _deleteAttachment(), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -10,10 +10,12 @@ class PendingAttachmentAltDialog extends StatefulWidget { | ||||
|   const PendingAttachmentAltDialog({super.key, required this.media}); | ||||
|  | ||||
|   @override | ||||
|   State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState(); | ||||
|   State<PendingAttachmentAltDialog> createState() => | ||||
|       _PendingAttachmentAltDialogState(); | ||||
| } | ||||
|  | ||||
| class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> { | ||||
| class _PendingAttachmentAltDialogState | ||||
|     extends State<PendingAttachmentAltDialog> { | ||||
|   final _contentController = TextEditingController(); | ||||
|  | ||||
|   @override | ||||
| @@ -63,7 +65,7 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> | ||||
|             controller: _contentController, | ||||
|             decoration: InputDecoration( | ||||
|               labelText: 'fieldAttachmentAlt'.tr(), | ||||
|               border: const UnderlineInputBorder(), | ||||
|               border: const OutlineInputBorder(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
| @@ -71,9 +73,11 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () { | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|           onPressed: _isBusy | ||||
|               ? null | ||||
|               : () { | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|           child: Text('dialogDismiss'.tr()), | ||||
|         ), | ||||
|         TextButton( | ||||
|   | ||||
| @@ -14,10 +14,12 @@ class PendingAttachmentBoostDialog extends StatefulWidget { | ||||
|   const PendingAttachmentBoostDialog({super.key, required this.media}); | ||||
|  | ||||
|   @override | ||||
|   State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState(); | ||||
|   State<PendingAttachmentBoostDialog> createState() => | ||||
|       _PendingAttachmentBoostDialogState(); | ||||
| } | ||||
|  | ||||
| class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> { | ||||
| class _PendingAttachmentBoostDialogState | ||||
|     extends State<PendingAttachmentBoostDialog> { | ||||
|   List<SnAttachmentDestination>? _regions; | ||||
|   SnAttachmentDestination? _selectedRegion; | ||||
|  | ||||
| @@ -84,17 +86,23 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia | ||||
|                     children: _regions!.map( | ||||
|                       (ele) { | ||||
|                         return RadioListTile( | ||||
|                           shape: RoundedRectangleBorder( | ||||
|                             borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                           ), | ||||
|                           title: Text(ele.label).tr(), | ||||
|                           subtitle: Text( | ||||
|                             'attachmentDestinationRegion${ele.region}'.trExists() | ||||
|                                 ? 'attachmentDestinationRegion${ele.region}'.tr() | ||||
|                                 ? 'attachmentDestinationRegion${ele.region}' | ||||
|                                     .tr() | ||||
|                                 : ele.region, | ||||
|                           ), | ||||
|                           selected: _selectedRegion == ele, | ||||
|                           value: ele, | ||||
|                           groupValue: _selectedRegion, | ||||
|                           onChanged: (value) { | ||||
|                             if (value != null) setState(() => _selectedRegion = value); | ||||
|                             if (value != null) { | ||||
|                               setState(() => _selectedRegion = value); | ||||
|                             } | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
| @@ -105,9 +113,11 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () { | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|           onPressed: _isBusy | ||||
|               ? null | ||||
|               : () { | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|           child: Text('dialogDismiss'.tr()), | ||||
|         ), | ||||
|         TextButton( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user