💄 Better attachment editor previewing

This commit is contained in:
LittleSheep 2024-08-01 16:45:18 +08:00
parent 9765b200b9
commit 1e4b44a78b
5 changed files with 178 additions and 181 deletions

View File

@ -112,7 +112,6 @@ class PostEditorController extends GetxController {
Future<void> editAttachment(BuildContext context) { Future<void> editAttachment(BuildContext context) {
return showModalBottomSheet( return showModalBottomSheet(
context: context, context: context,
isScrollControlled: true,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment', usage: 'i.attachment',
current: attachments, current: attachments,

View File

@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -16,6 +17,7 @@ import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
import 'package:solian/widgets/attachments/attachment_item.dart'; import 'package:solian/widgets/attachments/attachment_item.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
@ -42,7 +44,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<Attachment?> _attachments = List.empty(growable: true); List<Attachment?> _attachments = List.empty(growable: true);
Future<void> pickPhotoToUpload() async { Future<void> _pickPhotoToUpload() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -54,7 +56,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
for (final media in medias) { for (final media in medias) {
final file = File(media.path); final file = File(media.path);
try { try {
await uploadAttachment( await _uploadAttachment(
await file.readAsBytes(), await file.readAsBytes(),
file.path, file.path,
null, null,
@ -67,7 +69,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> pickVideoToUpload() async { Future<void> _pickVideoToUpload() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -79,7 +81,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
try { try {
await uploadAttachment(await file.readAsBytes(), file.path, null); await _uploadAttachment(await file.readAsBytes(), file.path, null);
} catch (err) { } catch (err) {
context.showErrorDialog(err); context.showErrorDialog(err);
} }
@ -87,7 +89,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> pickFileToUpload() async { Future<void> _pickFileToUpload() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -102,7 +104,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
for (final file in files) { for (final file in files) {
try { try {
await uploadAttachment(await file.readAsBytes(), file.path, null); await _uploadAttachment(await file.readAsBytes(), file.path, null);
} catch (err) { } catch (err) {
context.showErrorDialog(err); context.showErrorDialog(err);
} }
@ -111,7 +113,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> takeMediaToUpload(bool isVideo) async { Future<void> _takeMediaToUpload(bool isVideo) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -127,7 +129,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
try { try {
await uploadAttachment(await file.readAsBytes(), file.path, null); await _uploadAttachment(await file.readAsBytes(), file.path, null);
} catch (err) { } catch (err) {
context.showErrorDialog(err); context.showErrorDialog(err);
} }
@ -135,18 +137,18 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void pasteFileToUpload() async { void _pasteFileToUpload() async {
final data = await Pasteboard.image; final data = await Pasteboard.image;
if (data == null) return; if (data == null) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
uploadAttachment(data, 'Pasted Image', null); _uploadAttachment(data, 'Pasted Image', null);
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> uploadAttachment( Future<void> _uploadAttachment(
Uint8List data, String path, Map<String, dynamic>? metadata) async { Uint8List data, String path, Map<String, dynamic>? metadata) async {
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
try { try {
@ -188,7 +190,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
} }
void revertMetadataList() { void _revertMetadataList() {
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
if (widget.current.isEmpty) { if (widget.current.isEmpty) {
@ -215,7 +217,17 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
} }
} }
void showEdit(Attachment element, int index) { void _showAttachmentPreview(Attachment element) {
context.pushTransparentRoute(
AttachmentFullScreen(
parentId: 'attachment-editor-preview',
item: element,
),
rootNavigator: true,
);
}
void _showEdit(Attachment element, int index) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@ -234,15 +246,68 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
); );
} }
@override Widget _buildListEntry(Attachment element, int index) {
void initState() { var fileType = element.mimetype.split('/').firstOrNull;
super.initState(); fileType ??= 'unknown';
revertMetadataList();
final canBePreview = fileType.toLowerCase() == 'image';
return Container(
padding: const EdgeInsets.only(left: 16, right: 8, bottom: 16),
child: Card(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
SizedBox(
height: 54,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
element.alt,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'monospace'),
),
Text(
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}',
style: const TextStyle(fontSize: 12),
),
],
),
),
IconButton(
color: Colors.teal,
icon: const Icon(Icons.preview),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: canBePreview
? () => _showAttachmentPreview(element)
: null,
),
IconButton(
color: Theme.of(context).colorScheme.primary,
visualDensity: const VisualDensity(horizontal: -4),
icon: const Icon(Icons.more_horiz),
onPressed: () => _showEdit(element, index),
),
],
).paddingSymmetric(vertical: 8, horizontal: 16),
),
],
),
),
);
} }
@override @override
void dispose() { void initState() {
super.dispose(); super.initState();
_revertMetadataList();
} }
@override @override
@ -250,14 +315,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
const density = VisualDensity(horizontal: 0, vertical: 0); const density = VisualDensity(horizontal: 0, vertical: 0);
return SafeArea( return SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: DropTarget( child: DropTarget(
onDragDone: (detail) async { onDragDone: (detail) async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
for (final file in detail.files) { for (final file in detail.files) {
final data = await file.readAsBytes(); final data = await file.readAsBytes();
uploadAttachment(data, file.path, null); _uploadAttachment(data, file.path, null);
} }
setState(() => _isBusy = false); setState(() => _isBusy = false);
}, },
@ -281,71 +344,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
itemCount: _attachments.length, itemCount: _attachments.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _attachments[index]; final element = _attachments[index];
var fileType = element!.mimetype.split('/').firstOrNull; return _buildListEntry(element!, index);
fileType ??= 'unknown';
return Container(
padding: const EdgeInsets.only(
left: 16, right: 8, bottom: 16),
child: Card(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
SizedBox(
height: 280,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: AttachmentItem(
parentId: 'attachment-editor',
item: element,
showBadge: false,
showHideButton: false,
),
),
),
SizedBox(
height: 54,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
element.alt,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'monospace'),
),
Text(
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}',
style:
const TextStyle(fontSize: 12),
),
],
),
),
IconButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
foregroundColor:
Theme.of(context).primaryColor,
),
icon: const Icon(Icons.more_horiz),
onPressed: () => showEdit(element, index),
),
],
).paddingSymmetric(vertical: 8, horizontal: 16),
),
],
),
),
);
}, },
); );
}), }),
@ -361,42 +360,44 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
if (PlatformInfo.isDesktop || PlatformInfo.isIOS || PlatformInfo.isWeb) if (PlatformInfo.isDesktop ||
PlatformInfo.isIOS ||
PlatformInfo.isWeb)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.paste), icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr), label: Text('attachmentAddClipboard'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => pasteFileToUpload(), onPressed: () => _pasteFileToUpload(),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.add_photo_alternate), icon: const Icon(Icons.add_photo_alternate),
label: Text('attachmentAddGalleryPhoto'.tr), label: Text('attachmentAddGalleryPhoto'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => pickPhotoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.add_road), icon: const Icon(Icons.add_road),
label: Text('attachmentAddGalleryVideo'.tr), label: Text('attachmentAddGalleryVideo'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => pickVideoToUpload(), onPressed: () => _pickVideoToUpload(),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.photo_camera_back), icon: const Icon(Icons.photo_camera_back),
label: Text('attachmentAddCameraPhoto'.tr), label: Text('attachmentAddCameraPhoto'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.video_camera_back_outlined), icon: const Icon(Icons.video_camera_back_outlined),
label: Text('attachmentAddCameraVideo'.tr), label: Text('attachmentAddCameraVideo'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => takeMediaToUpload(true), onPressed: () => _takeMediaToUpload(true),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.file_present_rounded), icon: const Icon(Icons.file_present_rounded),
label: Text('attachmentAddFile'.tr), label: Text('attachmentAddFile'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => pickFileToUpload(), onPressed: () => _pickFileToUpload(),
), ),
], ],
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
@ -405,7 +406,6 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -16,19 +16,18 @@ import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart' show extension; import 'package:path/path.dart' show extension;
class AttachmentListFullScreen extends StatefulWidget { class AttachmentFullScreen extends StatefulWidget {
final String parentId; final String parentId;
final Attachment item; final Attachment item;
const AttachmentListFullScreen( const AttachmentFullScreen(
{super.key, required this.parentId, required this.item}); {super.key, required this.parentId, required this.item});
@override @override
State<AttachmentListFullScreen> createState() => State<AttachmentFullScreen> createState() => _AttachmentFullScreenState();
_AttachmentListFullScreenState();
} }
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> { class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
bool _showDetails = true; bool _showDetails = true;
bool _isDownloading = false; bool _isDownloading = false;

View File

@ -9,7 +9,7 @@ import 'package:get/get.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/widgets/attachments/attachment_item.dart'; import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_list_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
@ -320,7 +320,7 @@ class AttachmentListEntry extends StatelessWidget {
onReveal(true); onReveal(true);
} else if (['image'].contains(item!.mimetype.split('/').first)) { } else if (['image'].contains(item!.mimetype.split('/').first)) {
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentListFullScreen( AttachmentFullScreen(
parentId: parentId, parentId: parentId,
item: item!, item: item!,
), ),

View File

@ -46,7 +46,6 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
void _editAttachments() { void _editAttachments() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'm.attachment', usage: 'm.attachment',
current: _attachments, current: _attachments,