2024-11-11 13:48:50 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:ui';
|
|
|
|
|
|
|
|
import 'package:croppy/croppy.dart';
|
|
|
|
import 'package:dismissible_page/dismissible_page.dart';
|
2024-11-10 08:41:11 +00:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
2024-11-11 13:48:50 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
2024-11-10 08:41:11 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2024-12-24 16:48:19 +00:00
|
|
|
import 'package:flutter/services.dart';
|
2024-11-10 08:41:11 +00:00
|
|
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
|
|
|
import 'package:gap/gap.dart';
|
2024-12-28 11:23:49 +00:00
|
|
|
import 'package:google_fonts/google_fonts.dart';
|
2024-12-24 16:48:19 +00:00
|
|
|
import 'package:image_picker/image_picker.dart';
|
2024-11-10 08:41:11 +00:00
|
|
|
import 'package:material_symbols_icons/symbols.dart';
|
2024-12-24 16:48:19 +00:00
|
|
|
import 'package:pasteboard/pasteboard.dart';
|
2024-11-11 13:57:09 +00:00
|
|
|
import 'package:provider/provider.dart';
|
2024-11-11 13:30:05 +00:00
|
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
|
|
import 'package:surface/controllers/post_write_controller.dart';
|
2024-12-24 16:48:19 +00:00
|
|
|
import 'package:surface/providers/sn_attachment.dart';
|
2024-11-11 13:57:09 +00:00
|
|
|
import 'package:surface/providers/sn_network.dart';
|
2024-12-25 16:02:25 +00:00
|
|
|
import 'package:surface/types/attachment.dart';
|
|
|
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
2024-12-07 15:40:26 +00:00
|
|
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
2024-12-28 18:13:31 +00:00
|
|
|
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
2024-12-24 15:07:47 +00:00
|
|
|
import 'package:surface/widgets/context_menu.dart';
|
2024-11-11 13:57:09 +00:00
|
|
|
import 'package:surface/widgets/dialog.dart';
|
2024-12-25 16:02:25 +00:00
|
|
|
import 'package:surface/widgets/universal_image.dart';
|
2024-11-10 08:41:11 +00:00
|
|
|
|
2024-12-26 15:57:43 +00:00
|
|
|
import '../attachment/pending_attachment_compress.dart';
|
|
|
|
|
2024-11-10 08:41:11 +00:00
|
|
|
class PostMediaPendingList extends StatelessWidget {
|
2024-12-07 09:43:44 +00:00
|
|
|
final PostWriteMedia? thumbnail;
|
2024-11-17 16:55:39 +00:00
|
|
|
final List<PostWriteMedia> attachments;
|
|
|
|
final bool isBusy;
|
|
|
|
final Future<void> Function(int idx, PostWriteMedia updatedMedia)? onUpdate;
|
|
|
|
final Future<void> Function(int idx)? onRemove;
|
2024-12-07 09:43:44 +00:00
|
|
|
final Future<void> Function(int idx)? onUpload;
|
|
|
|
final void Function(int? idx)? onPostSetThumbnail;
|
|
|
|
final void Function(int idx)? onInsertLink;
|
2024-11-17 16:55:39 +00:00
|
|
|
final void Function(bool state)? onUpdateBusy;
|
|
|
|
|
2024-11-23 09:32:48 +00:00
|
|
|
const PostMediaPendingList({
|
2024-11-17 16:55:39 +00:00
|
|
|
super.key,
|
2024-12-07 09:43:44 +00:00
|
|
|
this.thumbnail,
|
2024-11-17 16:55:39 +00:00
|
|
|
required this.attachments,
|
|
|
|
required this.isBusy,
|
|
|
|
this.onUpdate,
|
|
|
|
this.onRemove,
|
2024-12-07 09:43:44 +00:00
|
|
|
this.onUpload,
|
|
|
|
this.onPostSetThumbnail,
|
|
|
|
this.onInsertLink,
|
2024-11-17 16:55:39 +00:00
|
|
|
this.onUpdateBusy,
|
|
|
|
});
|
|
|
|
|
|
|
|
Future<void> _cropImage(BuildContext context, int idx) async {
|
|
|
|
final media = attachments[idx];
|
2024-11-11 13:48:50 +00:00
|
|
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
|
|
|
? await showCupertinoImageCropper(
|
2024-11-18 14:52:22 +00:00
|
|
|
// ignore: use_build_context_synchronously
|
2024-11-11 13:48:50 +00:00
|
|
|
context,
|
2024-11-18 14:52:22 +00:00
|
|
|
// ignore: use_build_context_synchronously
|
2024-11-11 13:48:50 +00:00
|
|
|
imageProvider: media.getImageProvider(context)!,
|
|
|
|
)
|
|
|
|
: await showMaterialImageCropper(
|
2024-11-18 14:52:22 +00:00
|
|
|
// ignore: use_build_context_synchronously
|
2024-11-11 13:48:50 +00:00
|
|
|
context,
|
2024-11-18 14:52:22 +00:00
|
|
|
// ignore: use_build_context_synchronously
|
2024-11-11 13:48:50 +00:00
|
|
|
imageProvider: media.getImageProvider(context)!,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result == null) return;
|
2024-11-11 13:57:09 +00:00
|
|
|
|
2024-12-07 09:43:44 +00:00
|
|
|
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
2024-11-11 13:57:09 +00:00
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
if (onUpdate != null) {
|
|
|
|
final updatedMedia = PostWriteMedia.fromBytes(
|
|
|
|
rawBytes,
|
|
|
|
media.name,
|
|
|
|
media.type,
|
|
|
|
);
|
|
|
|
await onUpdate!(idx, updatedMedia);
|
|
|
|
}
|
2024-11-11 13:57:09 +00:00
|
|
|
}
|
|
|
|
|
2024-12-25 16:02:25 +00:00
|
|
|
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(),
|
2024-12-28 18:13:31 +00:00
|
|
|
analyzeNow: true,
|
2024-12-25 16:02:25 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
if (thumbnail == null) return;
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
2024-12-28 18:13:31 +00:00
|
|
|
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);
|
|
|
|
}
|
2024-12-25 16:02:25 +00:00
|
|
|
}
|
|
|
|
|
2024-11-17 16:55:39 +00:00
|
|
|
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
2024-12-07 09:43:44 +00:00
|
|
|
final media = idx == -1 ? thumbnail! : attachments[idx];
|
2024-11-11 13:57:09 +00:00
|
|
|
if (media.attachment == null) return;
|
|
|
|
|
|
|
|
try {
|
2024-11-17 16:55:39 +00:00
|
|
|
onUpdateBusy?.call(true);
|
2024-11-11 13:57:09 +00:00
|
|
|
final sn = context.read<SnNetworkProvider>();
|
|
|
|
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
2024-11-17 16:55:39 +00:00
|
|
|
onRemove!(idx);
|
2024-11-11 13:57:09 +00:00
|
|
|
} catch (err) {
|
|
|
|
if (!context.mounted) return;
|
|
|
|
context.showErrorDialog(err);
|
|
|
|
} finally {
|
2024-11-17 16:55:39 +00:00
|
|
|
onUpdateBusy?.call(false);
|
2024-11-11 13:57:09 +00:00
|
|
|
}
|
2024-11-11 13:48:50 +00:00
|
|
|
}
|
2024-11-10 08:41:11 +00:00
|
|
|
|
2024-12-28 18:13:31 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-12-26 15:57:43 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-12-24 15:07:47 +00:00
|
|
|
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
2024-12-26 15:57:43 +00:00
|
|
|
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
2024-11-23 09:32:48 +00:00
|
|
|
return ContextMenu(
|
|
|
|
entries: [
|
2024-12-26 15:57:43 +00:00
|
|
|
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
|
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentCompressVideo'.tr(),
|
|
|
|
icon: Symbols.compress,
|
|
|
|
onSelected: () {
|
|
|
|
_compressVideo(context, idx);
|
|
|
|
},
|
|
|
|
),
|
2024-12-28 18:13:31 +00:00
|
|
|
if (media.attachment != null)
|
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentBoost'.tr(),
|
|
|
|
icon: Symbols.bolt,
|
|
|
|
onSelected: () {
|
|
|
|
_createBoost(context, idx);
|
|
|
|
},
|
|
|
|
),
|
2024-12-26 14:19:01 +00:00
|
|
|
if (media.attachment != null && media.type == SnMediaType.video)
|
2024-12-24 16:48:19 +00:00
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentSetThumbnail'.tr(),
|
|
|
|
icon: Symbols.image,
|
2024-12-25 16:02:25 +00:00
|
|
|
onSelected: () {
|
|
|
|
_setThumbnail(context, idx);
|
|
|
|
},
|
2024-12-24 16:48:19 +00:00
|
|
|
),
|
2024-12-07 09:43:44 +00:00
|
|
|
if (media.attachment == null && onUpload != null)
|
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentUpload'.tr(),
|
|
|
|
icon: Symbols.upload,
|
|
|
|
onSelected: () {
|
|
|
|
onUpload!(idx);
|
|
|
|
}),
|
2024-12-26 15:21:33 +00:00
|
|
|
if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null && idx != -1)
|
2024-12-07 09:43:44 +00:00
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentSetAsPostThumbnail'.tr(),
|
|
|
|
icon: Symbols.gallery_thumbnail,
|
|
|
|
onSelected: () {
|
|
|
|
onPostSetThumbnail!(idx);
|
|
|
|
},
|
|
|
|
)
|
2024-12-26 14:19:01 +00:00
|
|
|
else if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null)
|
2024-12-07 09:43:44 +00:00
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentUnsetAsPostThumbnail'.tr(),
|
|
|
|
icon: Symbols.cancel,
|
|
|
|
onSelected: () {
|
|
|
|
onPostSetThumbnail!(null);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
if (media.attachment != null && onInsertLink != null)
|
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentInsertLink'.tr(),
|
|
|
|
icon: Symbols.add_link,
|
|
|
|
onSelected: () {
|
|
|
|
onInsertLink!(idx);
|
|
|
|
},
|
|
|
|
),
|
2024-12-26 14:19:01 +00:00
|
|
|
if (media.type == SnMediaType.image && media.attachment != null)
|
2024-11-23 09:32:48 +00:00
|
|
|
MenuItem(
|
|
|
|
label: 'preview'.tr(),
|
|
|
|
icon: Symbols.preview,
|
|
|
|
onSelected: () {
|
|
|
|
context.pushTransparentRoute(
|
2024-11-25 14:41:15 +00:00
|
|
|
AttachmentZoomView(data: [media.attachment!]),
|
2024-11-23 09:32:48 +00:00
|
|
|
rootNavigator: true,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
2024-12-26 14:19:01 +00:00
|
|
|
if (media.type == SnMediaType.image && media.attachment == null)
|
2024-11-23 09:32:48 +00:00
|
|
|
MenuItem(
|
|
|
|
label: 'crop'.tr(),
|
|
|
|
icon: Symbols.crop,
|
|
|
|
onSelected: () => _cropImage(context, idx),
|
|
|
|
),
|
2024-12-24 16:48:19 +00:00
|
|
|
if (media.attachment != null)
|
|
|
|
MenuItem(
|
|
|
|
label: 'attachmentCopyRandomId'.tr(),
|
|
|
|
icon: Symbols.content_copy,
|
|
|
|
onSelected: () {
|
|
|
|
Clipboard.setData(ClipboardData(text: media.attachment!.rid));
|
|
|
|
},
|
|
|
|
),
|
2024-11-23 09:32:48 +00:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-10 08:41:11 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2024-11-17 16:55:39 +00:00
|
|
|
return Container(
|
|
|
|
constraints: const BoxConstraints(maxHeight: 120),
|
2024-12-07 09:43:44 +00:00
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Gap(8),
|
|
|
|
if (thumbnail != null)
|
2024-12-24 15:07:47 +00:00
|
|
|
ContextMenuArea(
|
|
|
|
contextMenu: _createContextMenu(context, -1, thumbnail!),
|
2024-12-25 16:02:25 +00:00
|
|
|
child: _PostMediaPendingItem(media: thumbnail!),
|
2024-11-17 16:55:39 +00:00
|
|
|
),
|
2024-12-07 13:33:01 +00:00
|
|
|
if (thumbnail != null)
|
|
|
|
const VerticalDivider(width: 1, thickness: 1).padding(
|
|
|
|
horizontal: 12,
|
|
|
|
vertical: 16,
|
|
|
|
),
|
2024-12-07 09:43:44 +00:00
|
|
|
Expanded(
|
|
|
|
child: ListView.separated(
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
padding: const EdgeInsets.only(right: 8),
|
|
|
|
separatorBuilder: (context, index) => const Gap(8),
|
|
|
|
itemCount: attachments.length,
|
|
|
|
itemBuilder: (context, idx) {
|
|
|
|
final media = attachments[idx];
|
2024-12-24 15:07:47 +00:00
|
|
|
return ContextMenuArea(
|
|
|
|
contextMenu: _createContextMenu(context, idx, media),
|
2024-12-25 16:02:25 +00:00
|
|
|
child: _PostMediaPendingItem(media: media),
|
2024-12-07 09:43:44 +00:00
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2024-11-17 16:55:39 +00:00
|
|
|
),
|
2024-11-10 08:41:11 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2024-12-24 16:48:19 +00:00
|
|
|
|
2024-12-25 16:02:25 +00:00
|
|
|
class _PostMediaPendingItem extends StatelessWidget {
|
|
|
|
final PostWriteMedia media;
|
|
|
|
|
|
|
|
const _PostMediaPendingItem({
|
|
|
|
required this.media,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
|
|
|
|
|
|
final sn = context.read<SnNetworkProvider>();
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
border: Border.all(
|
|
|
|
color: Theme.of(context).dividerColor,
|
|
|
|
width: 1,
|
|
|
|
),
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
2024-12-28 11:23:49 +00:00
|
|
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
2024-12-25 16:02:25 +00:00
|
|
|
),
|
|
|
|
child: ClipRRect(
|
|
|
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
2024-12-28 11:23:49 +00:00
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
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.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(
|
|
|
|
offset: Offset(1, 1),
|
|
|
|
blurRadius: 8.0,
|
|
|
|
color: Color.fromARGB(255, 0, 0, 0),
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SnMediaType.audio => Stack(
|
|
|
|
fit: StackFit.expand,
|
|
|
|
children: [
|
|
|
|
if (media.attachment?.thumbnail != null)
|
|
|
|
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
|
|
|
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
|
|
|
Shadow(
|
|
|
|
offset: Offset(1, 1),
|
|
|
|
blurRadius: 8.0,
|
|
|
|
color: Color.fromARGB(255, 0, 0, 0),
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
_ => Container(
|
|
|
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
|
|
child: const Icon(Symbols.docs).center(),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
if (media.type != SnMediaType.image) const VerticalDivider(width: 1, thickness: 1),
|
|
|
|
if (media.type != SnMediaType.image)
|
|
|
|
SizedBox(
|
|
|
|
width: 160,
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
2024-12-26 15:21:33 +00:00
|
|
|
children: [
|
2024-12-28 11:23:49 +00:00
|
|
|
Expanded(
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
if (media.attachment != null)
|
|
|
|
Text(
|
|
|
|
media.attachment!.alt,
|
|
|
|
maxLines: 1,
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
)
|
|
|
|
else if (media.file != null)
|
|
|
|
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
|
|
|
|
else
|
|
|
|
Text('unknown'.tr()),
|
|
|
|
if (media.attachment != null)
|
|
|
|
Text(
|
|
|
|
media.attachment!.size.formatBytes(),
|
|
|
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
|
|
|
maxLines: 1,
|
|
|
|
)
|
|
|
|
else if (media.file != null)
|
|
|
|
FutureBuilder<int?>(
|
|
|
|
future: media.length(),
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
|
|
|
return Text(
|
|
|
|
snapshot.data!.formatBytes(),
|
|
|
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
|
|
|
maxLines: 1,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
2024-12-26 15:21:33 +00:00
|
|
|
),
|
2024-12-28 11:23:49 +00:00
|
|
|
),
|
2024-12-28 18:13:31 +00:00
|
|
|
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
|
2024-12-28 11:23:49 +00:00
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Icon(Symbols.bolt, size: 16),
|
|
|
|
const Gap(4),
|
2024-12-28 18:13:31 +00:00
|
|
|
Text('attachmentGotBoosted').tr().fontSize(13),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
if (media.attachment != null && media.attachment!.compressedId != null)
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Icon(Symbols.compress, size: 16),
|
|
|
|
const Gap(4),
|
2024-12-28 11:23:49 +00:00
|
|
|
Text('attachmentCopyCompressed').tr().fontSize(13),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
if (media.attachment != null)
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Icon(Symbols.cloud, size: 16),
|
|
|
|
const Gap(4),
|
|
|
|
Text('attachmentUploaded').tr().fontSize(13),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
else
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Icon(Symbols.cloud_off, size: 16),
|
|
|
|
const Gap(4),
|
|
|
|
Text('attachmentPending').tr().fontSize(13),
|
|
|
|
],
|
2024-12-26 15:57:43 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
2024-12-28 11:23:49 +00:00
|
|
|
).padding(horizontal: 12, vertical: 12),
|
|
|
|
],
|
2024-12-25 16:02:25 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-24 16:48:19 +00:00
|
|
|
class AddPostMediaButton extends StatelessWidget {
|
|
|
|
final Function(Iterable<PostWriteMedia>) onAdd;
|
|
|
|
|
|
|
|
const AddPostMediaButton({super.key, required this.onAdd});
|
|
|
|
|
|
|
|
void _takeMedia(bool isVideo) async {
|
|
|
|
final picker = ImagePicker();
|
|
|
|
final result = isVideo
|
|
|
|
? await picker.pickVideo(source: ImageSource.camera)
|
|
|
|
: await picker.pickImage(source: ImageSource.camera);
|
|
|
|
if (result == null) return;
|
|
|
|
onAdd([PostWriteMedia.fromFile(result)]);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _selectMedia() async {
|
|
|
|
final picker = ImagePicker();
|
|
|
|
final result = await picker.pickMultipleMedia();
|
|
|
|
if (result.isEmpty) return;
|
|
|
|
onAdd(
|
|
|
|
result.map((e) => PostWriteMedia.fromFile(e)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _pasteMedia() async {
|
|
|
|
final imageBytes = await Pasteboard.image;
|
|
|
|
if (imageBytes == null) return;
|
|
|
|
onAdd([
|
|
|
|
PostWriteMedia.fromBytes(
|
|
|
|
imageBytes,
|
|
|
|
'attachmentPastedImage'.tr(),
|
2024-12-26 14:19:01 +00:00
|
|
|
SnMediaType.image,
|
2024-12-24 16:48:19 +00:00
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _linkRandomId(BuildContext context) async {
|
|
|
|
final randomIdController = TextEditingController();
|
|
|
|
final randomId = await showDialog<String?>(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => AlertDialog(
|
|
|
|
title: Text('addAttachmentFromRandomId').tr(),
|
|
|
|
content: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
TextField(
|
|
|
|
controller: randomIdController,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
labelText: 'fieldAttachmentRandomId'.tr(),
|
|
|
|
border: const UnderlineInputBorder(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const Gap(8),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
child: Text('dialogDismiss').tr(),
|
|
|
|
onPressed: () {
|
|
|
|
Navigator.pop(context);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
TextButton(
|
|
|
|
child: Text('dialogConfirm').tr(),
|
|
|
|
onPressed: () {
|
|
|
|
Navigator.pop(context, randomIdController.text);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
randomIdController.dispose();
|
|
|
|
});
|
|
|
|
if (randomId == null || randomId.isEmpty) return;
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
|
|
|
final attach = context.read<SnAttachmentProvider>();
|
|
|
|
final attachment = await attach.getOne(randomId);
|
|
|
|
|
|
|
|
onAdd([
|
|
|
|
PostWriteMedia(attachment),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return PopupMenuButton(
|
|
|
|
icon: Icon(
|
|
|
|
Symbols.add_photo_alternate,
|
|
|
|
color: Theme.of(context).colorScheme.primary,
|
|
|
|
),
|
|
|
|
itemBuilder: (context) => [
|
|
|
|
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
|
|
PopupMenuItem(
|
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Icon(Symbols.photo_camera),
|
|
|
|
const Gap(16),
|
|
|
|
Text('addAttachmentFromCameraPhoto').tr(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
_takeMedia(false);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
|
|
PopupMenuItem(
|
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Icon(Symbols.videocam),
|
|
|
|
const Gap(16),
|
|
|
|
Text('addAttachmentFromCameraVideo').tr(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
_takeMedia(true);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
PopupMenuItem(
|
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Icon(Symbols.photo_library),
|
|
|
|
const Gap(16),
|
|
|
|
Text('addAttachmentFromAlbum').tr(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
_selectMedia();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
PopupMenuItem(
|
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Icon(Symbols.link),
|
|
|
|
const Gap(16),
|
|
|
|
Text('addAttachmentFromRandomId').tr(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
_linkRandomId(context);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
PopupMenuItem(
|
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
const Icon(Symbols.content_paste),
|
|
|
|
const Gap(16),
|
|
|
|
Text('addAttachmentFromClipboard').tr(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
_pasteMedia();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|