♻️ Refactored attachment meta editor
This commit is contained in:
		| @@ -458,7 +458,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> | ||||
|                           isBusy: _writeController.isBusy, | ||||
|                           onUpload: (int idx) async { | ||||
|                             await _writeController.uploadSingleAttachment( | ||||
|                                 context, idx); | ||||
|                               context, | ||||
|                               idx, | ||||
|                             ); | ||||
|                           }, | ||||
|                           onInsertLink: (int idx) async { | ||||
|                             _writeController.contentController.text += | ||||
|   | ||||
| @@ -30,6 +30,7 @@ abstract class SnAttachment with _$SnAttachment { | ||||
|     required String hash, | ||||
|     required int destination, | ||||
|     required int refCount, | ||||
|     String? refUrl, | ||||
|     @Default(0) int contentRating, | ||||
|     @Default(0) int qualityRating, | ||||
|     required DateTime? cleanedAt, | ||||
|   | ||||
| @@ -28,6 +28,7 @@ mixin _$SnAttachment { | ||||
|   String get hash; | ||||
|   int get destination; | ||||
|   int get refCount; | ||||
|   String? get refUrl; | ||||
|   int get contentRating; | ||||
|   int get qualityRating; | ||||
|   DateTime? get cleanedAt; | ||||
| @@ -83,6 +84,7 @@ mixin _$SnAttachment { | ||||
|                 other.destination == destination) && | ||||
|             (identical(other.refCount, refCount) || | ||||
|                 other.refCount == refCount) && | ||||
|             (identical(other.refUrl, refUrl) || other.refUrl == refUrl) && | ||||
|             (identical(other.contentRating, contentRating) || | ||||
|                 other.contentRating == contentRating) && | ||||
|             (identical(other.qualityRating, qualityRating) || | ||||
| @@ -132,6 +134,7 @@ mixin _$SnAttachment { | ||||
|         hash, | ||||
|         destination, | ||||
|         refCount, | ||||
|         refUrl, | ||||
|         contentRating, | ||||
|         qualityRating, | ||||
|         cleanedAt, | ||||
| @@ -155,7 +158,7 @@ mixin _$SnAttachment { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; | ||||
|     return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, refUrl: $refUrl, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -179,6 +182,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> { | ||||
|       String hash, | ||||
|       int destination, | ||||
|       int refCount, | ||||
|       String? refUrl, | ||||
|       int contentRating, | ||||
|       int qualityRating, | ||||
|       DateTime? cleanedAt, | ||||
| @@ -231,6 +235,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> { | ||||
|     Object? hash = null, | ||||
|     Object? destination = null, | ||||
|     Object? refCount = null, | ||||
|     Object? refUrl = freezed, | ||||
|     Object? contentRating = null, | ||||
|     Object? qualityRating = null, | ||||
|     Object? cleanedAt = freezed, | ||||
| @@ -304,6 +309,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> { | ||||
|           ? _self.refCount | ||||
|           : refCount // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       refUrl: freezed == refUrl | ||||
|           ? _self.refUrl | ||||
|           : refUrl // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       contentRating: null == contentRating | ||||
|           ? _self.contentRating | ||||
|           : contentRating // ignore: cast_nullable_to_non_nullable | ||||
| @@ -471,6 +480,7 @@ class _SnAttachment extends SnAttachment { | ||||
|       required this.hash, | ||||
|       required this.destination, | ||||
|       required this.refCount, | ||||
|       this.refUrl, | ||||
|       this.contentRating = 0, | ||||
|       this.qualityRating = 0, | ||||
|       required this.cleanedAt, | ||||
| @@ -524,6 +534,8 @@ class _SnAttachment extends SnAttachment { | ||||
|   @override | ||||
|   final int refCount; | ||||
|   @override | ||||
|   final String? refUrl; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final int contentRating; | ||||
|   @override | ||||
| @@ -623,6 +635,7 @@ class _SnAttachment extends SnAttachment { | ||||
|                 other.destination == destination) && | ||||
|             (identical(other.refCount, refCount) || | ||||
|                 other.refCount == refCount) && | ||||
|             (identical(other.refUrl, refUrl) || other.refUrl == refUrl) && | ||||
|             (identical(other.contentRating, contentRating) || | ||||
|                 other.contentRating == contentRating) && | ||||
|             (identical(other.qualityRating, qualityRating) || | ||||
| @@ -672,6 +685,7 @@ class _SnAttachment extends SnAttachment { | ||||
|         hash, | ||||
|         destination, | ||||
|         refCount, | ||||
|         refUrl, | ||||
|         contentRating, | ||||
|         qualityRating, | ||||
|         cleanedAt, | ||||
| @@ -695,7 +709,7 @@ class _SnAttachment extends SnAttachment { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; | ||||
|     return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, refUrl: $refUrl, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -721,6 +735,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res> | ||||
|       String hash, | ||||
|       int destination, | ||||
|       int refCount, | ||||
|       String? refUrl, | ||||
|       int contentRating, | ||||
|       int qualityRating, | ||||
|       DateTime? cleanedAt, | ||||
| @@ -779,6 +794,7 @@ class __$SnAttachmentCopyWithImpl<$Res> | ||||
|     Object? hash = null, | ||||
|     Object? destination = null, | ||||
|     Object? refCount = null, | ||||
|     Object? refUrl = freezed, | ||||
|     Object? contentRating = null, | ||||
|     Object? qualityRating = null, | ||||
|     Object? cleanedAt = freezed, | ||||
| @@ -852,6 +868,10 @@ class __$SnAttachmentCopyWithImpl<$Res> | ||||
|           ? _self.refCount | ||||
|           : refCount // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       refUrl: freezed == refUrl | ||||
|           ? _self.refUrl | ||||
|           : refUrl // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       contentRating: null == contentRating | ||||
|           ? _self.contentRating | ||||
|           : contentRating // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -23,6 +23,7 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) => | ||||
|       hash: json['hash'] as String, | ||||
|       destination: (json['destination'] as num).toInt(), | ||||
|       refCount: (json['ref_count'] as num).toInt(), | ||||
|       refUrl: json['ref_url'] as String?, | ||||
|       contentRating: (json['content_rating'] as num?)?.toInt() ?? 0, | ||||
|       qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0, | ||||
|       cleanedAt: json['cleaned_at'] == null | ||||
| @@ -75,6 +76,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) => | ||||
|       'hash': instance.hash, | ||||
|       'destination': instance.destination, | ||||
|       'ref_count': instance.refCount, | ||||
|       'ref_url': instance.refUrl, | ||||
|       'content_rating': instance.contentRating, | ||||
|       'quality_rating': instance.qualityRating, | ||||
|       'cleaned_at': instance.cleanedAt?.toIso8601String(), | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| // ignore_for_file: unused_import | ||||
|  | ||||
| import 'dart:io'; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| @@ -22,9 +24,9 @@ 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_actions.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_alt.dart'; | ||||
| import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| @@ -50,232 +52,6 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     this.onUpdateBusy, | ||||
|   }); | ||||
|  | ||||
|   Future<void> _cropImage(BuildContext context, int idx) async { | ||||
|     final media = attachments[idx]; | ||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|         ? await showCupertinoImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             // ignore: use_build_context_synchronously | ||||
|             imageProvider: media.getImageProvider(context)!, | ||||
|           ) | ||||
|         : await showMaterialImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
|             context, | ||||
|             // ignore: use_build_context_synchronously | ||||
|             imageProvider: media.getImageProvider(context)!, | ||||
|           ); | ||||
|  | ||||
|     if (result == null) return; | ||||
|  | ||||
|     final rawBytes = | ||||
|         (await result.uiImage.toByteData(format: ImageByteFormat.png))! | ||||
|             .buffer | ||||
|             .asUint8List(); | ||||
|  | ||||
|     if (onUpdate != null) { | ||||
|       final updatedMedia = PostWriteMedia.fromBytes( | ||||
|         rawBytes, | ||||
|         media.name, | ||||
|         media.type, | ||||
|       ); | ||||
|       await onUpdate!(idx, updatedMedia); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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(), | ||||
|         pool: 'interactive', | ||||
|         analyzeNow: true, | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     try { | ||||
|       final attach = context.read<SnAttachmentProvider>(); | ||||
|       final newAttach = await attach.updateOne( | ||||
|         attachments[idx].attachment!, | ||||
|         thumbnailId: thumbnail.id, | ||||
|       ); | ||||
|       onUpdate!(idx, PostWriteMedia(newAttach)); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment(BuildContext context, int idx) async { | ||||
|     final media = attachments[idx]; | ||||
|     if (media.attachment == null) return; | ||||
|  | ||||
|     try { | ||||
|       onUpdateBusy?.call(true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}'); | ||||
|       onRemove!(idx); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       onUpdateBusy?.call(false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _createBoost(BuildContext context, int idx) async { | ||||
|     if (attachments[idx].attachment == null) return; | ||||
|  | ||||
|     final result = await showDialog<SnAttachmentBoost?>( | ||||
|       context: context, | ||||
|       builder: (context) => | ||||
|           PendingAttachmentBoostDialog(media: attachments[idx]), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     final newAttach = attachments[idx].attachment!.copyWith( | ||||
|       boosts: [...attachments[idx].attachment!.boosts, result], | ||||
|     ); | ||||
|     final newMedia = PostWriteMedia(newAttach); | ||||
|  | ||||
|     onUpdate!(idx, newMedia); | ||||
|   } | ||||
|  | ||||
|   Future<void> _compressVideo(BuildContext context, int idx) async { | ||||
|     final result = await showDialog<PostWriteMedia?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingVideoCompressDialog(media: attachments[idx]), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     onUpdate!(idx, result); | ||||
|   } | ||||
|  | ||||
|   Future<void> _setAlt(BuildContext context, int idx) async { | ||||
|     final result = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]), | ||||
|     ); | ||||
|     if (result == null) return; | ||||
|  | ||||
|     onUpdate!(idx, PostWriteMedia(result)); | ||||
|   } | ||||
|  | ||||
|   ContextMenu _createContextMenu( | ||||
|       BuildContext context, int idx, PostWriteMedia media) { | ||||
|     final canCompressVideo = | ||||
|         !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); | ||||
|     return ContextMenu( | ||||
|       entries: [ | ||||
|         if (media.attachment == null && | ||||
|             media.type == SnMediaType.video && | ||||
|             canCompressVideo) | ||||
|           MenuItem( | ||||
|             label: 'attachmentCompressVideo'.tr(), | ||||
|             icon: Symbols.compress, | ||||
|             onSelected: () { | ||||
|               _compressVideo(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetAlt'.tr(), | ||||
|             icon: Symbols.description, | ||||
|             onSelected: () { | ||||
|               _setAlt(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentBoost'.tr(), | ||||
|             icon: Symbols.bolt, | ||||
|             onSelected: () { | ||||
|               _createBoost(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null && media.type == SnMediaType.video) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetThumbnail'.tr(), | ||||
|             icon: Symbols.image, | ||||
|             onSelected: () { | ||||
|               _setThumbnail(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment == null && onUpload != null) | ||||
|           MenuItem( | ||||
|               label: 'attachmentUpload'.tr(), | ||||
|               icon: Symbols.upload, | ||||
|               onSelected: () { | ||||
|                 onUpload!(idx); | ||||
|               }), | ||||
|         if (media.attachment != null && onInsertLink != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentInsertLink'.tr(), | ||||
|             icon: Symbols.add_link, | ||||
|             onSelected: () { | ||||
|               onInsertLink!(idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.type == SnMediaType.image && media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'preview'.tr(), | ||||
|             icon: Symbols.preview, | ||||
|             onSelected: () { | ||||
|               context.pushTransparentRoute( | ||||
|                 AttachmentZoomView(data: [media.attachment!]), | ||||
|                 rootNavigator: true, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.type == SnMediaType.image && media.attachment == null) | ||||
|           MenuItem( | ||||
|             label: 'crop'.tr(), | ||||
|             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(), | ||||
|             icon: Symbols.delete, | ||||
|             onSelected: isBusy ? null : () => _deleteAttachment(context, idx), | ||||
|           ), | ||||
|         if (media.attachment == null && onRemove != null) | ||||
|           MenuItem( | ||||
|             label: 'delete'.tr(), | ||||
|             icon: Symbols.delete, | ||||
|             onSelected: () { | ||||
|               onRemove!(idx); | ||||
|             }, | ||||
|           ) | ||||
|         else if (onRemove != null) | ||||
|           MenuItem( | ||||
|             label: 'unlink'.tr(), | ||||
|             icon: Symbols.link_off, | ||||
|             onSelected: () { | ||||
|               onRemove!(idx); | ||||
|             }, | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
| @@ -287,9 +63,27 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|         itemCount: attachments.length, | ||||
|         itemBuilder: (context, idx) { | ||||
|           final media = attachments[idx]; | ||||
|           return ContextMenuArea( | ||||
|             contextMenu: _createContextMenu(context, idx, media), | ||||
|           return GestureDetector( | ||||
|             child: _PostMediaPendingItem(media: media), | ||||
|             onTap: () { | ||||
|               showModalBottomSheet( | ||||
|                 context: context, | ||||
|                 builder: (context) => PendingAttachmentActionSheet( | ||||
|                   media: media, | ||||
|                 ), | ||||
|               ).then((value) async { | ||||
|                 if (value is PostWriteMedia) { | ||||
|                   await onUpdate!(idx, value); | ||||
|                 } | ||||
|                 if (value == 'link') { | ||||
|                   onInsertLink!(idx); | ||||
|                 } else if (value == false) { | ||||
|                   onRemove!(idx); | ||||
|                 } else if (value == true) { | ||||
|                   onUpload!(idx); | ||||
|                 } | ||||
|               }); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
| @@ -300,9 +94,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
| class _PostMediaPendingItem extends StatelessWidget { | ||||
|   final PostWriteMedia media; | ||||
|  | ||||
|   const _PostMediaPendingItem({ | ||||
|     required this.media, | ||||
|   }); | ||||
|   const _PostMediaPendingItem({required this.media}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -321,34 +113,36 @@ class _PostMediaPendingItem extends StatelessWidget { | ||||
|             AspectRatio( | ||||
|               aspectRatio: 1, | ||||
|               child: switch (media.type) { | ||||
|                 SnMediaType.image => | ||||
|                   LayoutBuilder(builder: (context, constraints) { | ||||
|                     return Image( | ||||
|                       image: media.getImageProvider( | ||||
|                         context, | ||||
|                         width: | ||||
|                             (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                         height: | ||||
|                             (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                       )!, | ||||
|                       fit: BoxFit.contain, | ||||
|                     ); | ||||
|                   }), | ||||
|                 SnMediaType.image => LayoutBuilder( | ||||
|                     builder: (context, constraints) { | ||||
|                       return Image( | ||||
|                         image: media.getImageProvider( | ||||
|                           context, | ||||
|                           width: | ||||
|                               (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                           height: (constraints.maxHeight * devicePixelRatio) | ||||
|                               .round(), | ||||
|                         )!, | ||||
|                         fit: BoxFit.contain, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 SnMediaType.video => Stack( | ||||
|                     fit: StackFit.expand, | ||||
|                     children: [ | ||||
|                       if (media.attachment?.thumbnail != null) | ||||
|                         AutoResizeUniversalImage(sn.getAttachmentUrl( | ||||
|                             media.attachment!.thumbnail!.rid)), | ||||
|                       const Icon(Symbols.videocam, | ||||
|                           color: Colors.white, | ||||
|                           shadows: [ | ||||
|                             Shadow( | ||||
|                       const Icon( | ||||
|                         Symbols.videocam, | ||||
|                         color: Colors.white, | ||||
|                         shadows: [ | ||||
|                           Shadow( | ||||
|                               offset: Offset(1, 1), | ||||
|                               blurRadius: 8.0, | ||||
|                               color: Color.fromARGB(255, 0, 0, 0), | ||||
|                             ), | ||||
|                           ]), | ||||
|                               color: Color.fromARGB(255, 0, 0, 0)) | ||||
|                         ], | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 SnMediaType.audio => Stack( | ||||
| @@ -357,15 +151,16 @@ class _PostMediaPendingItem extends StatelessWidget { | ||||
|                       if (media.attachment?.thumbnail != null) | ||||
|                         AutoResizeUniversalImage(sn.getAttachmentUrl( | ||||
|                             media.attachment!.thumbnail!.rid)), | ||||
|                       const Icon(Symbols.audio_file, | ||||
|                           color: Colors.white, | ||||
|                           shadows: [ | ||||
|                             Shadow( | ||||
|                       const Icon( | ||||
|                         Symbols.audio_file, | ||||
|                         color: Colors.white, | ||||
|                         shadows: [ | ||||
|                           Shadow( | ||||
|                               offset: Offset(1, 1), | ||||
|                               blurRadius: 8.0, | ||||
|                               color: Color.fromARGB(255, 0, 0, 0), | ||||
|                             ), | ||||
|                           ]), | ||||
|                               color: Color.fromARGB(255, 0, 0, 0)) | ||||
|                         ], | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 _ => Container( | ||||
| @@ -387,11 +182,8 @@ class _PostMediaPendingItem extends StatelessWidget { | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           if (media.attachment != null) | ||||
|                             Text( | ||||
|                               media.attachment!.alt, | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ) | ||||
|                             Text(media.attachment!.alt, | ||||
|                                 maxLines: 1, overflow: TextOverflow.ellipsis) | ||||
|                           else if (media.file != null) | ||||
|                             Text(media.file!.name, | ||||
|                                 maxLines: 1, overflow: TextOverflow.ellipsis) | ||||
| @@ -468,11 +260,8 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|   final VisualDensity? visualDensity; | ||||
|   final Function(Iterable<PostWriteMedia>) onAdd; | ||||
|  | ||||
|   const AddPostMediaButton({ | ||||
|     super.key, | ||||
|     required this.onAdd, | ||||
|     this.visualDensity, | ||||
|   }); | ||||
|   const AddPostMediaButton( | ||||
|       {super.key, required this.onAdd, this.visualDensity}); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final picker = ImagePicker(); | ||||
| @@ -487,17 +276,13 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     onAdd( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|     onAdd(result.map((e) => PostWriteMedia.fromFile(e))); | ||||
|   } | ||||
|  | ||||
|   void _selectFile() async { | ||||
|     final result = await FilePicker.platform.pickFiles(type: FileType.any); | ||||
|     if (result == null) return; | ||||
|     onAdd( | ||||
|       result.files.map((e) => PostWriteMedia.fromFile(e.xFile)), | ||||
|     ); | ||||
|     onAdd(result.files.map((e) => PostWriteMedia.fromFile(e.xFile))); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
| @@ -505,10 +290,7 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|     if (imageBytes == null) return; | ||||
|     onAdd([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         SnMediaType.image, | ||||
|       ), | ||||
|           imageBytes, 'attachmentPastedImage'.tr(), SnMediaType.image) | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
| @@ -556,19 +338,15 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final attachment = await attach.getOne(randomId); | ||||
|  | ||||
|     onAdd([ | ||||
|       PostWriteMedia(attachment), | ||||
|     ]); | ||||
|     onAdd([PostWriteMedia(attachment)]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return PopupMenuButton( | ||||
|       style: ButtonStyle(visualDensity: visualDensity), | ||||
|       icon: Icon( | ||||
|         Symbols.add_photo_alternate, | ||||
|         color: Theme.of(context).colorScheme.primary, | ||||
|       ), | ||||
|       icon: Icon(Symbols.add_photo_alternate, | ||||
|           color: Theme.of(context).colorScheme.primary), | ||||
|       itemBuilder: (context) => [ | ||||
|         if (!kIsWeb && | ||||
|             !Platform.isLinux && | ||||
| @@ -595,7 +373,7 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|               children: [ | ||||
|                 const Icon(Symbols.videocam), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraVideo').tr(), | ||||
|                 Text('addAttachmentFromCameraVideo').tr() | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
| @@ -607,7 +385,7 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|             children: [ | ||||
|               const Icon(Symbols.photo_library), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromAlbum').tr(), | ||||
|               Text('addAttachmentFromAlbum').tr() | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
| @@ -619,7 +397,7 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|             children: [ | ||||
|               const Icon(Symbols.file_upload), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromFiles').tr(), | ||||
|               Text('addAttachmentFromFiles').tr() | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
| @@ -627,13 +405,11 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.link), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromRandomId').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           child: Row(children: [ | ||||
|             const Icon(Symbols.link), | ||||
|             const Gap(16), | ||||
|             Text('addAttachmentFromRandomId').tr() | ||||
|           ]), | ||||
|           onTap: () { | ||||
|             _linkRandomId(context); | ||||
|           }, | ||||
| @@ -643,7 +419,7 @@ class AddPostMediaButton extends StatelessWidget { | ||||
|             children: [ | ||||
|               const Icon(Symbols.content_paste), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromClipboard').tr(), | ||||
|               Text('addAttachmentFromClipboard').tr() | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user