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:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.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_alt.dart'; import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; import 'package:surface/widgets/attachment/pending_attachment_compress.dart'; import 'package:surface/widgets/attachment/pending_attachment_rating.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; class PendingAttachmentActionSheet extends StatefulWidget { final PostWriteMedia media; const PendingAttachmentActionSheet({super.key, required this.media}); @override State createState() => _PendingAttachmentActionSheetState(); } class _PendingAttachmentActionSheetState extends State { bool _isBusy = false; Future _cropImage() async { final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ? await showCupertinoImageCropper( // ignore: use_build_context_synchronously context, // ignore: use_build_context_synchronously imageProvider: widget.media.getImageProvider(context)!, ) : await showMaterialImageCropper( // ignore: use_build_context_synchronously context, // ignore: use_build_context_synchronously imageProvider: widget.media.getImageProvider(context)!, ); if (result == null) return; final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))! .buffer .asUint8List(); if (!mounted) return; final updatedMedia = PostWriteMedia.fromBytes( rawBytes, widget.media.name, widget.media.type); Navigator.pop(context, updatedMedia); } Future _setThumbnail() async { final thumbnail = await showDialog( context: context, builder: (context) => AttachmentInputDialog( title: 'attachmentSetThumbnail'.tr(), pool: 'interactive', analyzeNow: true, ), ); if (thumbnail == null) return; if (!mounted) return; try { final attach = context.read(); final newAttach = await attach.updateOne(widget.media.attachment!, thumbnailId: thumbnail.id); if (mounted) Navigator.pop(context, newAttach); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } } Future _deleteAttachment() async { if (_isBusy) return; if (widget.media.attachment == null) return; try { setState(() => _isBusy = true); final sn = context.read(); await sn.client .delete('/cgi/uc/attachments/${widget.media.attachment!.id}'); if (!mounted) return; Navigator.pop(context, false); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _createBoost() async { final result = await showDialog( context: context, builder: (context) => PendingAttachmentBoostDialog( media: widget.media, ), ); if (result == null) return; final newAttach = widget.media.attachment! .copyWith(boosts: [...widget.media.attachment!.boosts, result]); final newMedia = PostWriteMedia(newAttach); if (!mounted) return; Navigator.pop(context, newMedia); } Future _compressVideo() async { final result = await showDialog( context: context, builder: (context) => PendingVideoCompressDialog(media: widget.media), ); if (result == null) return; if (!mounted) return; Navigator.pop(context, result); } Future _setAlt() async { final result = await showDialog( context: context, builder: (context) => PendingAttachmentAltDialog(media: widget.media), ); if (result == null) return; if (!mounted) return; Navigator.pop(context, PostWriteMedia(result)); } Future _setRating() async { final result = await showDialog( context: context, builder: (context) => PendingAttachmentRateDialog(media: widget.media), ); if (result == null) return; if (!mounted) return; Navigator.pop(context, PostWriteMedia(result)); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Symbols.edit, size: 24), const Gap(16), Text('attachmentEditor') .tr() .textStyle(Theme.of(context).textTheme.titleLarge!), ], ).padding(horizontal: 20, top: 16, bottom: 12), if (widget.media.attachment == null) Text('attachmentEditorUnUploadHint') .tr() .textStyle(Theme.of(context).textTheme.bodyMedium!) .padding(horizontal: 20, bottom: 8) .opacity(0.8) else Card( margin: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.media.attachment!.alt), Row( spacing: 6, children: [ Text(widget.media.attachment!.size.formatBytes()), Text( widget.media.attachment!.mimetype, style: GoogleFonts.robotoMono(), ), ], ), Text('attachmentEditorUploadHint') .tr() .textStyle(Theme.of(context).textTheme.bodyMedium!) .opacity(0.8), ], ).padding(horizontal: 16, vertical: 8), ).padding(horizontal: 16, bottom: 8), LoadingIndicator(isActive: _isBusy), if (widget.media.attachment == null) Expanded( child: ListView( children: [ ListTile( minTileHeight: 48, leading: const Icon(Symbols.upload), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentUpload').tr(), onTap: () => Navigator.pop(context, true), ), if (widget.media.type == SnMediaType.video) ListTile( minTileHeight: 48, leading: const Icon(Symbols.compress), title: Text('attachmentCompressVideo').tr(), onTap: () => _compressVideo(), ), if (widget.media.type == SnMediaType.image) ListTile( minTileHeight: 48, leading: const Icon(Symbols.crop), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('crop').tr(), onTap: () => _cropImage(), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.delete), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('delete').tr(), onTap: () => Navigator.pop(context, false), ), ], ), ) else Expanded( child: ListView( children: [ ListTile( minTileHeight: 48, leading: const Icon(Symbols.preview), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('preview').tr(), onTap: () { context.pushTransparentRoute( AttachmentZoomView(data: [widget.media.attachment!]), rootNavigator: true, ); }, ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.copy_all), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentCopyRandomId').tr(), onTap: () { Clipboard.setData( ClipboardData( text: widget.media.attachment!.rid, ), ); Navigator.pop(context); }, ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.add_link), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentInsertLink').tr(), onTap: () { Navigator.pop(context, 'link'); }, ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.bolt), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentBoost').tr(), onTap: () => _createBoost(), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.thumbnail_bar), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentSetThumbnail').tr(), onTap: () => _setThumbnail(), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.description), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentSetAlt').tr(), onTap: () => _setAlt(), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.star), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('attachmentRating').tr(), onTap: () => _setRating(), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.link_off), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('unlink').tr(), onTap: () => Navigator.pop(context, false), ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.delete), contentPadding: EdgeInsets.symmetric(horizontal: 24), title: Text('delete').tr(), onTap: () => _deleteAttachment(), ), ], ), ), ], ); } }