Surface/lib/widgets/post/post_media_pending_list.dart

433 lines
15 KiB
Dart

// 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<PostWriteMedia> attachments;
final bool isBusy;
final Future<void> Function(int idx, PostWriteMedia updatedMedia)? onUpdate;
final Future<void> Function(int idx)? onRemove;
final Future<void> 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<SnNetworkProvider>();
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<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,
);
},
),
],
),
),
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<PostWriteMedia>) 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<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(
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();
},
),
],
);
}
}