✨ Post with attachment
This commit is contained in:
		| @@ -30,6 +30,7 @@ | ||||
|   "create": "Create", | ||||
|   "preview": "Preview", | ||||
|   "loading": "Loading...", | ||||
|   "delete": "Delete", | ||||
|   "fieldUsername": "Username", | ||||
|   "fieldNickname": "Nickname", | ||||
|   "fieldEmail": "Email address", | ||||
|   | ||||
| @@ -30,6 +30,7 @@ | ||||
|   "apply": "应用", | ||||
|   "create": "创建", | ||||
|   "preview": "预览", | ||||
|   "delete": "删除", | ||||
|   "fieldUsername": "用户名", | ||||
|   "fieldNickname": "显示名", | ||||
|   "fieldEmail": "电子邮箱地址", | ||||
|   | ||||
| @@ -84,7 +84,12 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|         distance: 75, | ||||
|         type: ExpandableFabType.up, | ||||
|         childrenAnimation: ExpandableFabAnimation.none, | ||||
|         overlayStyle: ExpandableFabOverlayStyle(blur: 10), | ||||
|         overlayStyle: ExpandableFabOverlayStyle( | ||||
|           color: Theme.of(context) | ||||
|               .colorScheme | ||||
|               .surface | ||||
|               .withAlpha((255 * 0.5).round()), | ||||
|         ), | ||||
|         openButtonBuilder: RotateFloatingActionButtonBuilder( | ||||
|           child: const Icon(Symbols.add, size: 28), | ||||
|           fabSize: ExpandableFabSize.regular, | ||||
|   | ||||
| @@ -4,15 +4,19 @@ import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_media_pending_list.dart'; | ||||
| import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
|  | ||||
| class PostEditorScreen extends StatefulWidget { | ||||
|   final String mode; | ||||
| @@ -33,6 +37,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|   SnPublisher? _publisher; | ||||
|   List<SnPublisher>? _publishers; | ||||
|  | ||||
|   final List<XFile> _selectedMedia = List.empty(growable: true); | ||||
|   final List<SnAttachment> _attachments = List.empty(growable: true); | ||||
|  | ||||
|   void _fetchPublishers() async { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     final resp = await sn.client.get('/cgi/co/publishers'); | ||||
| @@ -49,19 +56,41 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|  | ||||
|   final TextEditingController _contentController = TextEditingController(); | ||||
|  | ||||
|   double? _progress; | ||||
|  | ||||
|   void _performAction() async { | ||||
|     if (_isBusy || _publisher == null) return; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     // Uploading attachments | ||||
|     try { | ||||
|       for (final media in _selectedMedia) { | ||||
|         final place = await attach.chunkedUploadInitialize( | ||||
|           await media.length(), | ||||
|           media.name, | ||||
|           'interactive', | ||||
|           null, | ||||
|         ); | ||||
|         final item = await attach.chunkedUploadParts(media, place.$1, place.$2); | ||||
|         _attachments.add(item); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       context.showErrorDialog(err); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Finishing up | ||||
|     try { | ||||
|       await sn.client.post('/cgi/co/${widget.mode}', data: { | ||||
|         'publisher': _publisher!.id, | ||||
|         'content': _contentController.value.text, | ||||
|         'title': _title, | ||||
|         'description': _description, | ||||
|         'attachments': _attachments.map((e) => e.rid).toList(), | ||||
|       }); | ||||
|       Navigator.pop(context, true); | ||||
|     } catch (err) { | ||||
| @@ -88,6 +117,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _selectedMedia.addAll(result); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _contentController.dispose(); | ||||
| @@ -248,13 +286,23 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           if (_selectedMedia.isNotEmpty) | ||||
|             PostMediaPendingList( | ||||
|               data: _selectedMedia, | ||||
|               onRemove: (idx) { | ||||
|                 setState(() { | ||||
|                   _selectedMedia.removeAt(idx); | ||||
|                 }); | ||||
|               }, | ||||
|             ).padding(bottom: 8), | ||||
|           Material( | ||||
|             elevation: 2, | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 if (_isBusy) | ||||
|                   const LinearProgressIndicator( | ||||
|                   LinearProgressIndicator( | ||||
|                     value: _progress, | ||||
|                     minHeight: 2, | ||||
|                   ), | ||||
|                 Row( | ||||
| @@ -268,7 +316,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                           child: Row( | ||||
|                             children: [ | ||||
|                               IconButton( | ||||
|                                 onPressed: () {}, | ||||
|                                 onPressed: _isBusy ? null : _selectMedia, | ||||
|                                 icon: Icon( | ||||
|                                   Symbols.add_photo_alternate, | ||||
|                                   color: Theme.of(context).colorScheme.primary, | ||||
|   | ||||
| @@ -21,7 +21,6 @@ class SnAttachment with _$SnAttachment { | ||||
|     required int refCount, | ||||
|     required dynamic fileChunks, | ||||
|     required dynamic cleanedAt, | ||||
|     required Map<String, dynamic> metadata, | ||||
|     required bool isMature, | ||||
|     required bool isAnalyzed, | ||||
|     required bool isUploaded, | ||||
| @@ -31,6 +30,7 @@ class SnAttachment with _$SnAttachment { | ||||
|     required SnAttachmentPool? pool, | ||||
|     required int poolId, | ||||
|     required int accountId, | ||||
|     @Default({}) Map<String, dynamic> metadata, | ||||
|   }) = _SnAttachment; | ||||
|  | ||||
|   factory SnAttachment.fromJson(Map<String, Object?> json) => | ||||
|   | ||||
| @@ -35,7 +35,6 @@ mixin _$SnAttachment { | ||||
|   int get refCount => throw _privateConstructorUsedError; | ||||
|   dynamic get fileChunks => throw _privateConstructorUsedError; | ||||
|   dynamic get cleanedAt => throw _privateConstructorUsedError; | ||||
|   Map<String, dynamic> get metadata => throw _privateConstructorUsedError; | ||||
|   bool get isMature => throw _privateConstructorUsedError; | ||||
|   bool get isAnalyzed => throw _privateConstructorUsedError; | ||||
|   bool get isUploaded => throw _privateConstructorUsedError; | ||||
| @@ -45,6 +44,7 @@ mixin _$SnAttachment { | ||||
|   SnAttachmentPool? get pool => throw _privateConstructorUsedError; | ||||
|   int get poolId => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|   Map<String, dynamic> get metadata => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnAttachment to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
| @@ -78,7 +78,6 @@ abstract class $SnAttachmentCopyWith<$Res> { | ||||
|       int refCount, | ||||
|       dynamic fileChunks, | ||||
|       dynamic cleanedAt, | ||||
|       Map<String, dynamic> metadata, | ||||
|       bool isMature, | ||||
|       bool isAnalyzed, | ||||
|       bool isUploaded, | ||||
| @@ -87,7 +86,8 @@ abstract class $SnAttachmentCopyWith<$Res> { | ||||
|       dynamic refId, | ||||
|       SnAttachmentPool? pool, | ||||
|       int poolId, | ||||
|       int accountId}); | ||||
|       int accountId, | ||||
|       Map<String, dynamic> metadata}); | ||||
|  | ||||
|   $SnAttachmentPoolCopyWith<$Res>? get pool; | ||||
| } | ||||
| @@ -122,7 +122,6 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> | ||||
|     Object? refCount = null, | ||||
|     Object? fileChunks = freezed, | ||||
|     Object? cleanedAt = freezed, | ||||
|     Object? metadata = null, | ||||
|     Object? isMature = null, | ||||
|     Object? isAnalyzed = null, | ||||
|     Object? isUploaded = null, | ||||
| @@ -132,6 +131,7 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> | ||||
|     Object? pool = freezed, | ||||
|     Object? poolId = null, | ||||
|     Object? accountId = null, | ||||
|     Object? metadata = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
| @@ -194,10 +194,6 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> | ||||
|           ? _value.cleanedAt | ||||
|           : cleanedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       metadata: null == metadata | ||||
|           ? _value.metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       isMature: null == isMature | ||||
|           ? _value.isMature | ||||
|           : isMature // ignore: cast_nullable_to_non_nullable | ||||
| @@ -234,6 +230,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       metadata: null == metadata | ||||
|           ? _value.metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|     ) as $Val); | ||||
|   } | ||||
|  | ||||
| @@ -276,7 +276,6 @@ abstract class _$$SnAttachmentImplCopyWith<$Res> | ||||
|       int refCount, | ||||
|       dynamic fileChunks, | ||||
|       dynamic cleanedAt, | ||||
|       Map<String, dynamic> metadata, | ||||
|       bool isMature, | ||||
|       bool isAnalyzed, | ||||
|       bool isUploaded, | ||||
| @@ -285,7 +284,8 @@ abstract class _$$SnAttachmentImplCopyWith<$Res> | ||||
|       dynamic refId, | ||||
|       SnAttachmentPool? pool, | ||||
|       int poolId, | ||||
|       int accountId}); | ||||
|       int accountId, | ||||
|       Map<String, dynamic> metadata}); | ||||
|  | ||||
|   @override | ||||
|   $SnAttachmentPoolCopyWith<$Res>? get pool; | ||||
| @@ -319,7 +319,6 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> | ||||
|     Object? refCount = null, | ||||
|     Object? fileChunks = freezed, | ||||
|     Object? cleanedAt = freezed, | ||||
|     Object? metadata = null, | ||||
|     Object? isMature = null, | ||||
|     Object? isAnalyzed = null, | ||||
|     Object? isUploaded = null, | ||||
| @@ -329,6 +328,7 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> | ||||
|     Object? pool = freezed, | ||||
|     Object? poolId = null, | ||||
|     Object? accountId = null, | ||||
|     Object? metadata = null, | ||||
|   }) { | ||||
|     return _then(_$SnAttachmentImpl( | ||||
|       id: null == id | ||||
| @@ -391,10 +391,6 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> | ||||
|           ? _value.cleanedAt | ||||
|           : cleanedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       metadata: null == metadata | ||||
|           ? _value._metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       isMature: null == isMature | ||||
|           ? _value.isMature | ||||
|           : isMature // ignore: cast_nullable_to_non_nullable | ||||
| @@ -431,6 +427,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       metadata: null == metadata | ||||
|           ? _value._metadata | ||||
|           : metadata // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| @@ -454,7 +454,6 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|       required this.refCount, | ||||
|       required this.fileChunks, | ||||
|       required this.cleanedAt, | ||||
|       required final Map<String, dynamic> metadata, | ||||
|       required this.isMature, | ||||
|       required this.isAnalyzed, | ||||
|       required this.isUploaded, | ||||
| @@ -463,7 +462,8 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|       required this.refId, | ||||
|       required this.pool, | ||||
|       required this.poolId, | ||||
|       required this.accountId}) | ||||
|       required this.accountId, | ||||
|       final Map<String, dynamic> metadata = const {}}) | ||||
|       : _metadata = metadata; | ||||
|  | ||||
|   factory _$SnAttachmentImpl.fromJson(Map<String, dynamic> json) => | ||||
| @@ -499,14 +499,6 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|   final dynamic fileChunks; | ||||
|   @override | ||||
|   final dynamic cleanedAt; | ||||
|   final Map<String, dynamic> _metadata; | ||||
|   @override | ||||
|   Map<String, dynamic> get metadata { | ||||
|     if (_metadata is EqualUnmodifiableMapView) return _metadata; | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableMapView(_metadata); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   final bool isMature; | ||||
|   @override | ||||
| @@ -525,10 +517,18 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|   final int poolId; | ||||
|   @override | ||||
|   final int accountId; | ||||
|   final Map<String, dynamic> _metadata; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   Map<String, dynamic> get metadata { | ||||
|     if (_metadata is EqualUnmodifiableMapView) return _metadata; | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableMapView(_metadata); | ||||
|   } | ||||
|  | ||||
|   @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, fileChunks: $fileChunks, cleanedAt: $cleanedAt, metadata: $metadata, isMature: $isMature, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId)'; | ||||
|     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, fileChunks: $fileChunks, cleanedAt: $cleanedAt, isMature: $isMature, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, metadata: $metadata)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -557,7 +557,6 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.fileChunks, fileChunks) && | ||||
|             const DeepCollectionEquality().equals(other.cleanedAt, cleanedAt) && | ||||
|             const DeepCollectionEquality().equals(other._metadata, _metadata) && | ||||
|             (identical(other.isMature, isMature) || | ||||
|                 other.isMature == isMature) && | ||||
|             (identical(other.isAnalyzed, isAnalyzed) || | ||||
| @@ -571,7 +570,8 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|             (identical(other.pool, pool) || other.pool == pool) && | ||||
|             (identical(other.poolId, poolId) || other.poolId == poolId) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId)); | ||||
|                 other.accountId == accountId) && | ||||
|             const DeepCollectionEquality().equals(other._metadata, _metadata)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -593,7 +593,6 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|         refCount, | ||||
|         const DeepCollectionEquality().hash(fileChunks), | ||||
|         const DeepCollectionEquality().hash(cleanedAt), | ||||
|         const DeepCollectionEquality().hash(_metadata), | ||||
|         isMature, | ||||
|         isAnalyzed, | ||||
|         isUploaded, | ||||
| @@ -602,7 +601,8 @@ class _$SnAttachmentImpl implements _SnAttachment { | ||||
|         const DeepCollectionEquality().hash(refId), | ||||
|         pool, | ||||
|         poolId, | ||||
|         accountId | ||||
|         accountId, | ||||
|         const DeepCollectionEquality().hash(_metadata) | ||||
|       ]); | ||||
|  | ||||
|   /// Create a copy of SnAttachment | ||||
| @@ -638,7 +638,6 @@ abstract class _SnAttachment implements SnAttachment { | ||||
|       required final int refCount, | ||||
|       required final dynamic fileChunks, | ||||
|       required final dynamic cleanedAt, | ||||
|       required final Map<String, dynamic> metadata, | ||||
|       required final bool isMature, | ||||
|       required final bool isAnalyzed, | ||||
|       required final bool isUploaded, | ||||
| @@ -647,7 +646,8 @@ abstract class _SnAttachment implements SnAttachment { | ||||
|       required final dynamic refId, | ||||
|       required final SnAttachmentPool? pool, | ||||
|       required final int poolId, | ||||
|       required final int accountId}) = _$SnAttachmentImpl; | ||||
|       required final int accountId, | ||||
|       final Map<String, dynamic> metadata}) = _$SnAttachmentImpl; | ||||
|  | ||||
|   factory _SnAttachment.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnAttachmentImpl.fromJson; | ||||
| @@ -683,8 +683,6 @@ abstract class _SnAttachment implements SnAttachment { | ||||
|   @override | ||||
|   dynamic get cleanedAt; | ||||
|   @override | ||||
|   Map<String, dynamic> get metadata; | ||||
|   @override | ||||
|   bool get isMature; | ||||
|   @override | ||||
|   bool get isAnalyzed; | ||||
| @@ -702,6 +700,8 @@ abstract class _SnAttachment implements SnAttachment { | ||||
|   int get poolId; | ||||
|   @override | ||||
|   int get accountId; | ||||
|   @override | ||||
|   Map<String, dynamic> get metadata; | ||||
|  | ||||
|   /// Create a copy of SnAttachment | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   | ||||
| @@ -23,7 +23,6 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) => | ||||
|       refCount: (json['ref_count'] as num).toInt(), | ||||
|       fileChunks: json['file_chunks'], | ||||
|       cleanedAt: json['cleaned_at'], | ||||
|       metadata: json['metadata'] as Map<String, dynamic>, | ||||
|       isMature: json['is_mature'] as bool, | ||||
|       isAnalyzed: json['is_analyzed'] as bool, | ||||
|       isUploaded: json['is_uploaded'] as bool, | ||||
| @@ -35,6 +34,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) => | ||||
|           : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), | ||||
|       poolId: (json['pool_id'] as num).toInt(), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|       metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
| @@ -54,7 +54,6 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
|       'ref_count': instance.refCount, | ||||
|       'file_chunks': instance.fileChunks, | ||||
|       'cleaned_at': instance.cleanedAt, | ||||
|       'metadata': instance.metadata, | ||||
|       'is_mature': instance.isMature, | ||||
|       'is_analyzed': instance.isAnalyzed, | ||||
|       'is_uploaded': instance.isUploaded, | ||||
| @@ -64,6 +63,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => | ||||
|       'pool': instance.pool?.toJson(), | ||||
|       'pool_id': instance.poolId, | ||||
|       'account_id': instance.accountId, | ||||
|       'metadata': instance.metadata, | ||||
|     }; | ||||
|  | ||||
| _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( | ||||
|   | ||||
| @@ -14,9 +14,9 @@ class AttachmentItem extends StatelessWidget { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     switch (tp) { | ||||
|       case 'image': | ||||
|         return AspectRatio( | ||||
|           aspectRatio: data.metadata['ratio']?.toDouble(), | ||||
|           child: UniversalImage(sn.getAttachmentUrl(data.rid)), | ||||
|         return UniversalImage( | ||||
|           sn.getAttachmentUrl(data.rid), | ||||
|           fit: BoxFit.cover, | ||||
|         ); | ||||
|       default: | ||||
|         return const Placeholder(); | ||||
|   | ||||
| @@ -8,8 +8,12 @@ class AttachmentList extends StatelessWidget { | ||||
|   final List<SnAttachment> data; | ||||
|   final bool? bordered; | ||||
|   final double? maxListHeight; | ||||
|   const AttachmentList( | ||||
|       {super.key, required this.data, this.bordered, this.maxListHeight}); | ||||
|   const AttachmentList({ | ||||
|     super.key, | ||||
|     required this.data, | ||||
|     this.bordered, | ||||
|     this.maxListHeight, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -37,13 +41,19 @@ class AttachmentList extends StatelessWidget { | ||||
|           itemBuilder: (context, idx) { | ||||
|             const radius = BorderRadius.all(Radius.circular(8)); | ||||
|             return Container( | ||||
|               constraints: BoxConstraints( | ||||
|                 maxWidth: MediaQuery.of(context).size.width - 20, | ||||
|               ), | ||||
|               decoration: BoxDecoration( | ||||
|                 border: Border(top: borderSide, bottom: borderSide), | ||||
|                 borderRadius: radius, | ||||
|               ), | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: radius, | ||||
|                 child: AttachmentItem(data: data[idx]), | ||||
|               child: AspectRatio( | ||||
|                 aspectRatio: data[idx].metadata['ratio']?.toDouble() ?? 1, | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: radius, | ||||
|                   child: AttachmentItem(data: data[idx]), | ||||
|                 ), | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|   | ||||
							
								
								
									
										67
									
								
								lib/widgets/post/post_media_pending_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								lib/widgets/post/post_media_pending_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:cross_file/cross_file.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| class PostMediaPendingList extends StatelessWidget { | ||||
|   final List<XFile> data; | ||||
|   final Function(int idx)? onRemove; | ||||
|   const PostMediaPendingList({ | ||||
|     super.key, | ||||
|     required this.data, | ||||
|     this.onRemove, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       constraints: const BoxConstraints(maxHeight: 120), | ||||
|       child: ListView.separated( | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|         separatorBuilder: (context, index) => const Gap(8), | ||||
|         itemCount: data.length, | ||||
|         itemBuilder: (context, idx) { | ||||
|           final file = data[idx]; | ||||
|           return ContextMenuRegion( | ||||
|             contextMenu: ContextMenu( | ||||
|               entries: [ | ||||
|                 if (onRemove != null) | ||||
|                   MenuItem( | ||||
|                     label: 'delete'.tr(), | ||||
|                     icon: Symbols.delete, | ||||
|                     onSelected: () { | ||||
|                       onRemove!(idx); | ||||
|                     }, | ||||
|                   ), | ||||
|               ], | ||||
|             ), | ||||
|             child: Container( | ||||
|               decoration: BoxDecoration( | ||||
|                 border: Border.all( | ||||
|                   color: Theme.of(context).dividerColor, | ||||
|                   width: 1, | ||||
|                 ), | ||||
|                 borderRadius: BorderRadius.circular(8), | ||||
|               ), | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 child: AspectRatio( | ||||
|                   aspectRatio: 1, | ||||
|                   child: kIsWeb | ||||
|                       ? Image.network(file.path, fit: BoxFit.cover) | ||||
|                       : Image.file(File(file.path), fit: BoxFit.cover), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -467,6 +467,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.1" | ||||
|   flutter_context_menu: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_context_menu | ||||
|       sha256: "4bc1dc30ae5aa705ed99ebbeb875898c6341a6d092397a566fecd5184b392380" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|   flutter_expandable_fab: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -66,6 +66,7 @@ dependencies: | ||||
|   croppy: ^1.3.1 | ||||
|   flutter_expandable_fab: ^2.3.0 | ||||
|   dropdown_button2: ^2.3.9 | ||||
|   flutter_context_menu: ^0.2.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user