// ignore_for_file: unused_import 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:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:image_picker/image_picker.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:pasteboard/pasteboard.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_actions.dart'; import 'package:surface/widgets/attachment/pending_attachment_alt.dart'; import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; import '../attachment/pending_attachment_compress.dart'; class PostMediaPendingList extends StatelessWidget { final List attachments; final bool isBusy; final Future Function(int idx, PostWriteMedia updatedMedia)? onUpdate; final Future Function(int idx)? onRemove; final Future Function(int idx)? onUpload; final void Function(int idx)? onInsertLink; final void Function(bool state)? onUpdateBusy; const PostMediaPendingList({ super.key, required this.attachments, required this.isBusy, this.onUpdate, this.onRemove, this.onUpload, this.onInsertLink, this.onUpdateBusy, }); @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: attachments.length, itemBuilder: (context, idx) { final media = attachments[idx]; 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); } }); }, ); }, ), ); } } 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(); return Material( elevation: 4, color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(8), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), 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, children: [ 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( 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, ); }, ), ], ), ), if (media.attachment != null && media.attachment!.boosts.isNotEmpty) Row( children: [ Icon(Symbols.bolt, size: 16), const Gap(4), Text('attachmentGotBoosted').tr().fontSize(13), ], ), if (media.attachment != null && media.attachment!.compressedId != null) Row( children: [ Icon(Symbols.compress, size: 16), const Gap(4), 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), ], ), ], ), ).padding(horizontal: 12, vertical: 12), ], ), ), ); } } class AddPostMediaButton extends StatelessWidget { final VisualDensity? visualDensity; final Function(Iterable) onAdd; const AddPostMediaButton( {super.key, required this.onAdd, this.visualDensity}); 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 _selectFile() async { final result = await FilePicker.platform.pickFiles(type: FileType.any); if (result == null) return; onAdd(result.files.map((e) => PostWriteMedia.fromFile(e.xFile))); } void _pasteMedia() async { final imageBytes = await Pasteboard.image; if (imageBytes == null) return; onAdd([ PostWriteMedia.fromBytes( imageBytes, 'attachmentPastedImage'.tr(), SnMediaType.image) ]); } void _linkRandomId(BuildContext context) async { final randomIdController = TextEditingController(); final randomId = await showDialog( 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(); final attachment = await attach.getOne(randomId); 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), 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.file_upload), const Gap(16), Text('addAttachmentFromFiles').tr() ], ), onTap: () { _selectFile(); }, ), 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(); }, ), ], ); } }