♻️ Refactored attachment meta editor
This commit is contained in:
@ -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