Compare commits

..

3 Commits

Author SHA1 Message Date
ab73916795 Detailed attachments 2024-11-10 17:21:57 +08:00
b53cb9fc81 💫 Better posting progress 2024-11-10 16:52:24 +08:00
ef51948fad Post with attachment 2024-11-10 16:41:11 +08:00
13 changed files with 364 additions and 61 deletions

View File

@ -30,6 +30,7 @@
"create": "Create",
"preview": "Preview",
"loading": "Loading...",
"delete": "Delete",
"fieldUsername": "Username",
"fieldNickname": "Nickname",
"fieldEmail": "Email address",

View File

@ -30,6 +30,7 @@
"apply": "应用",
"create": "创建",
"preview": "预览",
"delete": "删除",
"fieldUsername": "用户名",
"fieldNickname": "显示名",
"fieldEmail": "电子邮箱地址",

View File

@ -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,

View File

@ -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,22 +56,82 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final TextEditingController _contentController = TextEditingController();
double? _progress;
static const kAttachmentProgressWeight = 0.9;
static const kPostingProgressWeight = 0.1;
void _performAction() async {
if (_isBusy || _publisher == null) return;
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
setState(() => _isBusy = true);
setState(() {
_progress = 0;
_isBusy = true;
});
// Uploading attachments
try {
for (int i = 0; i < _selectedMedia.length; i++) {
final media = _selectedMedia[i];
final place = await attach.chunkedUploadInitialize(
await media.length(),
media.name,
'interactive',
null,
);
final item = await attach.chunkedUploadParts(
media,
place.$1,
place.$2,
onProgress: (progress) {
// Calculate overall progress for attachments
setState(() {
_progress = ((i + progress) / _selectedMedia.length) *
kAttachmentProgressWeight;
});
},
);
_attachments.add(item);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
return;
}
setState(() => _progress = kAttachmentProgressWeight);
// Posting the content
try {
final baseProgressVal = _progress!;
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(),
}, onSendProgress: (count, total) {
setState(() {
_progress =
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
});
}, onReceiveProgress: (count, total) {
setState(() {
_progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
});
});
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
@ -88,6 +155,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,15 +324,29 @@ 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(
minHeight: 2,
),
if (_isBusy && _progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300),
builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -268,7 +358,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,

View File

@ -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) =>

View File

@ -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.

View File

@ -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(

View File

@ -0,0 +1,37 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
class AttachmentDetailPopup extends StatelessWidget {
final SnAttachment data;
final String? heroTag;
const AttachmentDetailPopup({super.key, required this.data, this.heroTag});
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final uuid = Uuid();
return DismissiblePage(
onDismissed: () {
Navigator.of(context).pop();
},
direction: DismissiblePageDismissDirection.down,
backgroundColor: Colors.transparent,
isFullScreen: true,
child: Hero(
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
child: PhotoView(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(data.rid),
),
),
),
);
}
}

View File

@ -1,25 +1,55 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_detail.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
class AttachmentItem extends StatelessWidget {
final SnAttachment data;
const AttachmentItem({super.key, required this.data});
final bool isExpandable;
const AttachmentItem({
super.key,
required this.data,
this.isExpandable = false,
});
@override
Widget build(BuildContext context) {
Widget _buildContent(BuildContext context, String heroTag) {
final tp = data.mimetype.split('/').firstOrNull;
final sn = context.read<SnNetworkProvider>();
switch (tp) {
case 'image':
return AspectRatio(
aspectRatio: data.metadata['ratio']?.toDouble(),
child: UniversalImage(sn.getAttachmentUrl(data.rid)),
return Hero(
tag: 'attachment-${data.rid}-$heroTag',
child: UniversalImage(
sn.getAttachmentUrl(data.rid),
fit: BoxFit.cover,
),
);
default:
return const Placeholder();
}
}
@override
Widget build(BuildContext context) {
final uuid = Uuid();
final heroTag = uuid.v4();
if (isExpandable) {
return GestureDetector(
child: _buildContent(context, heroTag),
onTap: () {
context.pushTransparentRoute(
AttachmentDetailPopup(data: data, heroTag: heroTag),
rootNavigator: true,
);
},
);
}
return _buildContent(context, heroTag);
}
}

View File

@ -1,6 +1,9 @@
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
@ -8,8 +11,16 @@ 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,
});
static const double kMaxListItemWidth = 520;
static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8));
@override
Widget build(BuildContext context) {
@ -19,11 +30,36 @@ class AttachmentList extends StatelessWidget {
if (data.isEmpty) return const SizedBox.shrink();
if (data.length == 1) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
MediaQuery.of(context).size.width - 20,
kMaxListItemWidth,
),
),
decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide),
borderRadius: kDefaultRadius,
),
child: AspectRatio(
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
child: ClipRRect(
borderRadius: kDefaultRadius,
child: AttachmentItem(data: data[0], isExpandable: true),
),
),
);
}
return Container(
decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide),
),
child: AttachmentItem(data: data[0]),
child: AspectRatio(
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
child: AttachmentItem(data: data[0], isExpandable: true),
),
);
}
@ -35,15 +71,23 @@ class AttachmentList extends StatelessWidget {
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (context, idx) {
const radius = BorderRadius.all(Radius.circular(8));
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
MediaQuery.of(context).size.width - 20,
kMaxListItemWidth,
),
),
decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide),
borderRadius: radius,
borderRadius: kDefaultRadius,
),
child: ClipRRect(
borderRadius: radius,
child: AttachmentItem(data: data[idx]),
child: AspectRatio(
aspectRatio: data[idx].metadata['ratio']?.toDouble() ?? 1,
child: ClipRRect(
borderRadius: kDefaultRadius,
child: AttachmentItem(data: data[idx], isExpandable: true),
),
),
);
},

View 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),
),
),
),
);
},
),
);
}
}

View File

@ -334,6 +334,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
dismissible_page:
dependency: "direct main"
description:
name: dismissible_page
sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dropdown_button2:
dependency: "direct main"
description:
@ -467,6 +475,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:
@ -1026,6 +1042,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
platform:
dependency: transitive
description:
@ -1408,7 +1432,7 @@ packages:
source: hosted
version: "3.1.3"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff

View File

@ -66,6 +66,10 @@ dependencies:
croppy: ^1.3.1
flutter_expandable_fab: ^2.3.0
dropdown_button2: ^2.3.9
flutter_context_menu: ^0.2.0
dismissible_page: ^1.0.2
uuid: ^4.5.1
photo_view: ^0.15.0
dev_dependencies:
flutter_test: