From cecb94b2e12c8ed5e4357efc2e94f6bbadce8b9b Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:26:33 +0930 Subject: [PATCH 1/6] Improve file sending flow --- commet/lib/client/attachment.dart | 14 +- .../matrix_url_preview_component.dart | 2 +- commet/lib/config/preferences.dart | 17 +- commet/lib/ui/molecules/file_preview.dart | 55 +++++ commet/lib/ui/molecules/message_input.dart | 21 +- commet/lib/ui/navigation/adaptive_dialog.dart | 16 +- .../attachment_processor.dart | 195 ++++++++++++++++++ commet/lib/ui/organisms/chat/chat.dart | 47 ++++- commet/lib/utils/mime.dart | 4 +- commet/pubspec.lock | 14 +- commet/pubspec.yaml | 2 + tiamat/lib/atoms/popup_dialog.dart | 16 +- 12 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 commet/lib/ui/molecules/file_preview.dart create mode 100644 commet/lib/ui/organisms/attachment_processor/attachment_processor.dart diff --git a/commet/lib/client/attachment.dart b/commet/lib/client/attachment.dart index 4b22b627..f2b040ca 100644 --- a/commet/lib/client/attachment.dart +++ b/commet/lib/client/attachment.dart @@ -24,7 +24,7 @@ class PendingFileAttachment { {this.name, this.path, this.data, this.mimeType, this.size}) { assert(path != null || data != null); - mimeType ??= Mime.lookupType(path ?? "", data: data); + mimeType ??= Mime.lookupType(path ?? name ?? "", data: data); } Future resolve() async { @@ -38,6 +38,18 @@ class PendingFileAttachment { } } } + + ImageProvider? getAsImage() { + if (Mime.imageTypes.contains(mimeType)) { + if (data != null) { + return Image.memory(data!).image; + } else { + return Image.file(File(path!)).image; + } + } + + return null; + } } class ImageAttachment implements Attachment { diff --git a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart index e908b13d..48995c3c 100644 --- a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart +++ b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart @@ -173,7 +173,7 @@ pQIDAQAB var type = response["og:image:type"] as String?; if (type != null) { - if (Mime.displayableTypes.contains(type) == false) { + if (Mime.displayableImageTypes.contains(type) == false) { imageUrl = null; } } diff --git a/commet/lib/config/preferences.dart b/commet/lib/config/preferences.dart index 05127995..58e53f64 100644 --- a/commet/lib/config/preferences.dart +++ b/commet/lib/config/preferences.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:commet/config/build_config.dart'; +import 'package:commet/config/platform_utils.dart'; import 'package:commet/config/theme_config.dart'; import 'package:commet/main.dart'; import 'package:flutter/material.dart'; @@ -104,13 +105,15 @@ class Preferences { WidgetsBinding.instance.platformDispatcher.platformBrightness; } - var custom = await ThemeConfig.getThemeByName(preferences.theme); - if (custom != null) { - var jsonString = await custom.readAsString(); - var json = const JsonDecoder().convert(jsonString); - var themedata = await ThemeJsonConverter.fromJson(json, custom); - if (themedata != null) { - return themedata; + if (!PlatformUtils.isWeb) { + var custom = await ThemeConfig.getThemeByName(preferences.theme); + if (custom != null) { + var jsonString = await custom.readAsString(); + var json = const JsonDecoder().convert(jsonString); + var themedata = await ThemeJsonConverter.fromJson(json, custom); + if (themedata != null) { + return themedata; + } } } diff --git a/commet/lib/ui/molecules/file_preview.dart b/commet/lib/ui/molecules/file_preview.dart new file mode 100644 index 00000000..38a39cce --- /dev/null +++ b/commet/lib/ui/molecules/file_preview.dart @@ -0,0 +1,55 @@ +import 'dart:convert' show utf8; +import 'dart:io'; + +import 'package:commet/ui/atoms/code_block.dart'; +import 'package:commet/utils/mime.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class FilePreview extends StatefulWidget { + const FilePreview({required this.mimeType, this.path, this.data, super.key}); + final String? mimeType; + final String? path; + final Uint8List? data; + + @override + State createState() => _FilePreviewState(); +} + +class _FilePreviewState extends State { + ImageProvider? image; + String? text; + + @override + void initState() { + if (widget.mimeType != null) { + if (Mime.displayableImageTypes.contains(widget.mimeType)) { + if (widget.data != null) { + image = Image.memory(widget.data!).image; + } else { + image = Image.file(File(widget.path!)).image; + } + } else if (Mime.isText(widget.mimeType!) && widget.data != null) { + text = utf8.decode(widget.data!); + } + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (image != null) { + return Image( + image: image!, + filterQuality: FilterQuality.medium, + fit: BoxFit.contain, + ); + } + if (text != null) { + return Codeblock(text: text!); + } + return const SizedBox( + height: 0, + ); + } +} diff --git a/commet/lib/ui/molecules/message_input.dart b/commet/lib/ui/molecules/message_input.dart index 5fcd8ef2..e258cc65 100644 --- a/commet/lib/ui/molecules/message_input.dart +++ b/commet/lib/ui/molecules/message_input.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'package:commet/client/components/gif/gif_component.dart'; import 'package:commet/config/build_config.dart'; +import 'package:commet/config/platform_utils.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/atoms/emoji_widget.dart'; import 'package:commet/ui/atoms/rich_text_field.dart'; import 'package:commet/ui/molecules/attachment_icon.dart'; +import 'package:commet/ui/organisms/attachment_processor/attachment_processor.dart'; import 'package:commet/ui/molecules/emoticon_picker.dart'; +import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/organisms/chat/chat.dart'; import 'package:commet/client/components/emoticon/emoji_pack.dart'; import 'package:commet/client/components/gif/gif_search_result.dart'; @@ -634,10 +637,24 @@ class MessageInputState extends State { for (var file in result.files) { var attachment = PendingFileAttachment( name: file.name, - path: file.path, + path: PlatformUtils.isWeb ? null : file.path, data: file.bytes, size: file.bytes?.length); - widget.addAttachment?.call(attachment); + if (mounted) { + var processedFile = await AdaptiveDialog.show( + scrollable: false, + context, + builder: (context) { + return AttachmentProcessor( + attachment: attachment, + ); + }, + ); + + if (processedFile != null) { + widget.addAttachment?.call(processedFile); + } + } } } } diff --git a/commet/lib/ui/navigation/adaptive_dialog.dart b/commet/lib/ui/navigation/adaptive_dialog.dart index e84cd71e..4a8740f7 100644 --- a/commet/lib/ui/navigation/adaptive_dialog.dart +++ b/commet/lib/ui/navigation/adaptive_dialog.dart @@ -10,13 +10,16 @@ class AdaptiveDialog { static Future show( BuildContext context, { required Widget Function(BuildContext context) builder, - required String title, + String? title, + bool scrollable = true, bool dismissible = true, double initialHeightMobile = 0.5, }) async { if (Layout.desktop) { return PopupDialog.show(context, - content: SingleChildScrollView(child: builder(context)), + content: scrollable + ? SingleChildScrollView(child: builder(context)) + : builder(context), title: title, barrierDismissible: dismissible); } @@ -39,10 +42,11 @@ class AdaptiveDialog { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: tiamat.Text.largeTitle(title), - ), + if (title != null) + Padding( + padding: const EdgeInsets.all(12.0), + child: tiamat.Text.largeTitle(title), + ), Center(child: builder(context)), ], ), diff --git a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart new file mode 100644 index 00000000..4fb6fa97 --- /dev/null +++ b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart @@ -0,0 +1,195 @@ +import 'dart:io'; + +import 'package:commet/client/attachment.dart'; +import 'package:commet/ui/molecules/file_preview.dart'; +import 'package:commet/utils/mime.dart'; +import 'package:exif/exif.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart' as path; +import 'package:image/image.dart' as img; +import 'package:tiamat/tiamat.dart' as tiamat; + +class AttachmentProcessor extends StatefulWidget { + const AttachmentProcessor({required this.attachment, super.key}); + final PendingFileAttachment attachment; + + @override + State createState() => _AttachmentProcessorState(); +} + +class _AttachmentProcessorState extends State { + String get promptAttachmentProcessingSendOriginal => Intl.message( + "Send Original", + name: "promptAttachmentProcessingSendOriginal", + desc: + "Prompt text for the option to send a file in its original state, without any further processing such as removing metadata"); + + String get labelImageContainsLocationInfo => Intl.message( + "Warning: This image contains location metadata", + name: "labelImageContainsLocationInfo", + desc: + "Prompt text for the option to send a file in its original state, without any further processing such as removing metadata"); + + Map? exifData; + late IconData icon; + + bool canProcessData = false; + bool containsGpsData = false; + bool sendOriginalFile = false; + + @override + void initState() { + icon = Mime.toIcon(widget.attachment.mimeType); + if (Mime.imageTypes.contains(widget.attachment.mimeType)) { + loadExif(); + canProcessData = true; + + if (widget.attachment.mimeType == "image/gif") { + canProcessData = false; + } + } + super.initState(); + } + + void loadExif() async { + late Map data; + if (widget.attachment.data != null) { + data = await readExifFromBytes(widget.attachment.data!); + } else { + data = await readExifFromFile(File(widget.attachment.path!)); + } + + setState(() { + if (data.keys.any((e) => e.toLowerCase().contains("gps"))) { + containsGpsData = true; + } + + exifData = data; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.attachment.name != null) + Row( + children: [ + Icon(icon), + tiamat.Text.labelLow(widget.attachment.name!), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(500, 500)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: FilePreview( + mimeType: widget.attachment.mimeType, + path: widget.attachment.path, + data: widget.attachment.data, + ), + ), + ), + ), + ), + if (canProcessData) + Padding( + padding: const EdgeInsets.all(8.0), + child: buildFileProcessingSwitch(), + ), + if (sendOriginalFile || !canProcessData) buildMetadataDisplay(), + buildConfirmButton(), + ], + ); + } + + Widget buildFileProcessingSwitch() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + tiamat.Text.label(promptAttachmentProcessingSendOriginal), + tiamat.Switch( + state: sendOriginalFile, + onChanged: (value) => setState(() { + sendOriginalFile = value; + }), + ), + ], + ); + } + + Widget buildMetadataDisplay() { + return Column( + children: [ + if (containsGpsData) tiamat.Text.error(labelImageContainsLocationInfo) + ], + ); + } + + Widget buildConfirmButton() { + return tiamat.Button( + text: "Add File", + onTap: submit, + ); + } + + void submit() async { + if (canProcessData == false || sendOriginalFile) { + Navigator.of(context).pop(widget.attachment); + } else { + var file = await processFile(); + if (mounted) { + Navigator.of(context).pop(file); + } + } + } + + Future processFile() async { + late PendingFileAttachment processedFile; + + if (Mime.imageTypes.contains(widget.attachment.mimeType)) { + processedFile = await processImage(); + } + + return processedFile; + } + + Future processImage() async { + var data = widget.attachment.data ?? + await File(widget.attachment.path!).readAsBytes(); + + var decoder = img.findDecoderForData(data); + var image = decoder!.decode(data)!; + + image.exif.clear(); + + Uint8List? processedData; + String? name = widget.attachment.name; + String mime = widget.attachment.mimeType!; + if (widget.attachment.name != null) { + processedData = img.encodeNamedImage(widget.attachment.name!, image); + } + + if (processedData == null) { + processedData = img.encodePng(image); + mime = "image/png"; + var fileName = widget.attachment.name ?? "untitled.png"; + var rawName = path.basenameWithoutExtension(fileName); + name = "$rawName.png"; + } + + return PendingFileAttachment( + name: name, + data: processedData, + size: processedData.lengthInBytes, + mimeType: mime); + } +} diff --git a/commet/lib/ui/organisms/chat/chat.dart b/commet/lib/ui/organisms/chat/chat.dart index 16121152..397f02c2 100644 --- a/commet/lib/ui/organisms/chat/chat.dart +++ b/commet/lib/ui/organisms/chat/chat.dart @@ -13,11 +13,13 @@ import 'package:commet/client/components/typing_indicators/typing_indicator_comp import 'package:commet/client/room.dart'; import 'package:commet/client/timeline.dart'; import 'package:commet/debug/log.dart'; +import 'package:commet/ui/organisms/attachment_processor/attachment_processor.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/organisms/chat/chat_view.dart'; import 'package:commet/utils/debounce.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:desktop_drop/desktop_drop.dart'; +import 'package:exif/exif.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -172,6 +174,29 @@ class ChatState extends State { processing = true; }); + for (var file in attachments) { + await file.resolve(); + var exif = await readExifFromBytes(file.data!); + + if (exif.keys.any((e) => e.toLowerCase().contains("gps"))) { + // ignore: use_build_context_synchronously + var confirmation = await AdaptiveDialog.confirmation(context, + title: file.name ?? "File", + confirmationText: "Send File", + cancelText: "Don't send file", + dangerous: true, + prompt: + "Location data was detected in file '${file.name}', are you sure you want to send?"); + + if (confirmation != true) { + setState(() { + processing = false; + }); + return; + } + } + } + var processedAttachments = await room.processAttachments(attachments); setState(() { @@ -308,10 +333,24 @@ class ChatState extends State { if (size < 50000000) { data = await file.readAsBytes(); } - setState(() { - attachments.add(PendingFileAttachment( - name: file.name, path: file.path, size: size, data: data)); - }); + + if (mounted) { + var attachment = PendingFileAttachment( + name: file.name, path: file.path, size: size, data: data); + + var processedAttachment = + await AdaptiveDialog.show(context, + scrollable: false, + builder: (context) => AttachmentProcessor( + attachment: attachment, + )); + + if (processedAttachment != null) { + setState(() { + attachments.add(processedAttachment); + }); + } + } } } } diff --git a/commet/lib/utils/mime.dart b/commet/lib/utils/mime.dart index dcbd36a3..c4274290 100644 --- a/commet/lib/utils/mime.dart +++ b/commet/lib/utils/mime.dart @@ -6,7 +6,7 @@ import 'package:matrix/matrix.dart'; import 'package:mime/mime.dart' as mime; class Mime { - static const displayableTypes = { + static const displayableImageTypes = { "image/jpeg", "image/png", "image/gif", @@ -22,6 +22,8 @@ class Mime { "image/bmp", }; + static bool isText(String mime) => mime.startsWith("text/"); + static const videoTypes = {"video/mp4", "video/mpeg"}; static const archiveTypes = { diff --git a/commet/pubspec.lock b/commet/pubspec.lock index 3f75f7d8..8e3bf1e5 100644 --- a/commet/pubspec.lock +++ b/commet/pubspec.lock @@ -395,6 +395,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.4" + exif: + dependency: "direct main" + description: + name: exif + sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172 + url: "https://pub.dev" + source: hosted + version: "3.3.0" fake_async: dependency: transitive description: @@ -734,13 +742,13 @@ packages: source: hosted version: "4.0.2" image: - dependency: transitive + dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" implicitly_animated_list: dependency: "direct main" description: diff --git a/commet/pubspec.yaml b/commet/pubspec.yaml index c99db441..b630880d 100644 --- a/commet/pubspec.yaml +++ b/commet/pubspec.yaml @@ -86,6 +86,8 @@ dependencies: flutter_web_auth_2: ^3.1.1 drift: ^2.18.0 archive: ^3.6.1 + exif: ^3.3.0 + image: ^4.2.0 ## ---- Putting some extra lines in here to help git with the diff ## ---- Probably good to keep this stuff at the bottom of the list diff --git a/tiamat/lib/atoms/popup_dialog.dart b/tiamat/lib/atoms/popup_dialog.dart index 305c40a9..3dadbda7 100644 --- a/tiamat/lib/atoms/popup_dialog.dart +++ b/tiamat/lib/atoms/popup_dialog.dart @@ -46,7 +46,7 @@ class PopupDialog extends StatelessWidget { required this.content, this.width = null, this.height = null}); - final String title; + final String? title; final double? width; final double? height; final Widget content; @@ -55,7 +55,7 @@ class PopupDialog extends StatelessWidget { static Future show(BuildContext context, {required Widget content, - required String title, + String? title, double? width, double? height, bool barrierDismissible = true}) { @@ -89,11 +89,13 @@ class PopupDialog extends StatelessWidget { elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), contentPadding: const EdgeInsets.all(8), - title: Row( - children: [ - Text(title), - ], - ), + title: title == null + ? null + : Row( + children: [ + Text(title!), + ], + ), content: content, ); } From cd1cc00b5ad310c278a7ff9a9a000ee6d3c7b314 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:28:36 +0930 Subject: [PATCH 2/6] Update attachment_processor.dart --- .../attachment_processor.dart | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart index 4fb6fa97..5125b58d 100644 --- a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart +++ b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:commet/client/attachment.dart'; +import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/ui/molecules/file_preview.dart'; import 'package:commet/utils/mime.dart'; import 'package:exif/exif.dart'; @@ -73,41 +74,43 @@ class _AttachmentProcessorState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.attachment.name != null) - Row( - children: [ - Icon(icon), - tiamat.Text.labelLow(widget.attachment.name!), - ], - ), - Flexible( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: BoxConstraints.loose(const Size(500, 500)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: FilePreview( - mimeType: widget.attachment.mimeType, - path: widget.attachment.path, - data: widget.attachment.data, + return ScaledSafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.attachment.name != null) + Row( + children: [ + Icon(icon), + tiamat.Text.labelLow(widget.attachment.name!), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(500, 500)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: FilePreview( + mimeType: widget.attachment.mimeType, + path: widget.attachment.path, + data: widget.attachment.data, + ), ), ), ), ), - ), - if (canProcessData) - Padding( - padding: const EdgeInsets.all(8.0), - child: buildFileProcessingSwitch(), - ), - if (sendOriginalFile || !canProcessData) buildMetadataDisplay(), - buildConfirmButton(), - ], + if (canProcessData) + Padding( + padding: const EdgeInsets.all(8.0), + child: buildFileProcessingSwitch(), + ), + if (sendOriginalFile || !canProcessData) buildMetadataDisplay(), + buildConfirmButton(), + ], + ), ); } From 3650211a6bca73ebc5cdcc9c103c7d7bb7eb6241 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:56:34 +0930 Subject: [PATCH 3/6] process images in isolate --- .../attachment_processor.dart | 123 ++++++++++-------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart index 5125b58d..4e06607b 100644 --- a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart +++ b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart @@ -41,6 +41,8 @@ class _AttachmentProcessorState extends State { bool containsGpsData = false; bool sendOriginalFile = false; + bool processing = false; + @override void initState() { icon = Mime.toIcon(widget.attachment.mimeType); @@ -75,40 +77,53 @@ class _AttachmentProcessorState extends State { @override Widget build(BuildContext context) { return ScaledSafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, + child: Stack( + alignment: Alignment.center, children: [ - if (widget.attachment.name != null) - Row( - children: [ - Icon(icon), - tiamat.Text.labelLow(widget.attachment.name!), - ], - ), - Flexible( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: BoxConstraints.loose(const Size(500, 500)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: FilePreview( - mimeType: widget.attachment.mimeType, - path: widget.attachment.path, - data: widget.attachment.data, + Opacity( + opacity: processing ? 0.5 : 1, + child: IgnorePointer( + ignoring: processing, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.attachment.name != null) + Row( + children: [ + Icon(icon), + tiamat.Text.labelLow(widget.attachment.name!), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(500, 500)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: FilePreview( + mimeType: widget.attachment.mimeType, + path: widget.attachment.path, + data: widget.attachment.data, + ), + ), + ), + ), ), - ), + if (canProcessData) + Padding( + padding: const EdgeInsets.all(8.0), + child: buildFileProcessingSwitch(), + ), + if (sendOriginalFile || !canProcessData) + buildMetadataDisplay(), + buildConfirmButton(), + ], ), ), ), - if (canProcessData) - Padding( - padding: const EdgeInsets.all(8.0), - child: buildFileProcessingSwitch(), - ), - if (sendOriginalFile || !canProcessData) buildMetadataDisplay(), - buildConfirmButton(), + if (processing) const CircularProgressIndicator() ], ), ); @@ -148,6 +163,9 @@ class _AttachmentProcessorState extends State { if (canProcessData == false || sendOriginalFile) { Navigator.of(context).pop(widget.attachment); } else { + setState(() { + processing = true; + }); var file = await processFile(); if (mounted) { Navigator.of(context).pop(file); @@ -166,33 +184,34 @@ class _AttachmentProcessorState extends State { } Future processImage() async { - var data = widget.attachment.data ?? - await File(widget.attachment.path!).readAsBytes(); + return await compute((PendingFileAttachment attachment) async { + var data = attachment.data ?? await File(attachment.path!).readAsBytes(); - var decoder = img.findDecoderForData(data); - var image = decoder!.decode(data)!; + var decoder = img.findDecoderForData(data); + var image = decoder!.decode(data)!; - image.exif.clear(); + image.exif.clear(); - Uint8List? processedData; - String? name = widget.attachment.name; - String mime = widget.attachment.mimeType!; - if (widget.attachment.name != null) { - processedData = img.encodeNamedImage(widget.attachment.name!, image); - } + Uint8List? processedData; + String? name = attachment.name; + String mime = attachment.mimeType!; + if (attachment.name != null) { + processedData = img.encodeNamedImage(attachment.name!, image); + } - if (processedData == null) { - processedData = img.encodePng(image); - mime = "image/png"; - var fileName = widget.attachment.name ?? "untitled.png"; - var rawName = path.basenameWithoutExtension(fileName); - name = "$rawName.png"; - } + if (processedData == null) { + processedData = img.encodePng(image); + mime = "image/png"; + var fileName = attachment.name ?? "untitled.png"; + var rawName = path.basenameWithoutExtension(fileName); + name = "$rawName.png"; + } - return PendingFileAttachment( - name: name, - data: processedData, - size: processedData.lengthInBytes, - mimeType: mime); + return PendingFileAttachment( + name: name, + data: processedData, + size: processedData.lengthInBytes, + mimeType: mime); + }, widget.attachment); } } From 14ea4fc1f2579fbf1a4a77d19988f962242882c5 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:30:21 +0930 Subject: [PATCH 4/6] extract video metadata and thumbnails --- commet/lib/cache/file_provider.dart | 21 ++++++++ commet/lib/client/attachment.dart | 5 ++ .../lib/client/matrix/matrix_attachment.dart | 4 +- commet/lib/client/matrix/matrix_room.dart | 48 ++++++++++++++----- commet/lib/ui/atoms/message_attachment.dart | 1 + commet/lib/ui/molecules/attachment_icon.dart | 20 +++++--- commet/lib/ui/molecules/file_preview.dart | 30 +++++++++++- .../molecules/video_player/video_player.dart | 21 ++++++-- .../video_player/video_player_controller.dart | 19 ++++++++ .../video_player_implementation.dart | 24 +++++++++- .../attachment_processor.dart | 24 ++++++++++ 11 files changed, 192 insertions(+), 25 deletions(-) diff --git a/commet/lib/cache/file_provider.dart b/commet/lib/cache/file_provider.dart index e89a2825..aece8e76 100644 --- a/commet/lib/cache/file_provider.dart +++ b/commet/lib/cache/file_provider.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + abstract class FileProvider { Future resolve(); @@ -5,3 +7,22 @@ abstract class FileProvider { String get fileIdentifier; } + +class SystemFileProvider implements FileProvider { + File file; + + @override + String get fileIdentifier => file.path; + + @override + Future resolve() async { + return file.uri; + } + + @override + Future save(String filepath) { + throw UnimplementedError(); + } + + SystemFileProvider(this.file); +} diff --git a/commet/lib/client/attachment.dart b/commet/lib/client/attachment.dart index f2b040ca..d9ba8ff7 100644 --- a/commet/lib/client/attachment.dart +++ b/commet/lib/client/attachment.dart @@ -20,6 +20,11 @@ class PendingFileAttachment { String? mimeType; int? size; + Uint8List? thumbnailFile; + String? thumbnailMime; + Size? dimensions; + Duration? length; + PendingFileAttachment( {this.name, this.path, this.data, this.mimeType, this.size}) { assert(path != null || data != null); diff --git a/commet/lib/client/matrix/matrix_attachment.dart b/commet/lib/client/matrix/matrix_attachment.dart index f9ac23b1..a759cfd8 100644 --- a/commet/lib/client/matrix/matrix_attachment.dart +++ b/commet/lib/client/matrix/matrix_attachment.dart @@ -4,5 +4,7 @@ import 'package:matrix/matrix.dart'; class MatrixProcessedAttachment extends ProcessedAttachment { MatrixFile file; - MatrixProcessedAttachment(this.file); + MatrixImageFile? thumbnailFile; + + MatrixProcessedAttachment(this.file, {this.thumbnailFile}); } diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index afd14edf..3e7c4fc6 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -279,12 +279,11 @@ class MatrixRoom extends Room { Future> processAttachments( List attachments) async { return await Future.wait(attachments.map((e) async { - var file = await processAttachment(e); - return MatrixProcessedAttachment(file!); + return (await processAttachment(e))!; })); } - Future processAttachment( + Future processAttachment( PendingFileAttachment attachment) async { await attachment.resolve(); if (attachment.data == null) return null; @@ -300,13 +299,12 @@ class MatrixRoom extends Room { try { if (Mime.imageTypes.contains(attachment.mimeType)) { await decodeImageFromList(attachment.data!); - - return await matrix.MatrixImageFile.create( + return MatrixProcessedAttachment(await matrix.MatrixImageFile.create( bytes: attachment.data!, name: attachment.name ?? "unknown", mimeType: attachment.mimeType, nativeImplementations: - (client as MatrixClient).nativeImplentations); + (client as MatrixClient).nativeImplentations)); } } catch (error, stack) { // This image is probably corrupt, since it has a mime type we should be able to display, @@ -315,10 +313,37 @@ class MatrixRoom extends Room { Log.onError(error, stack); } - return matrix.MatrixFile( - bytes: attachment.data!, - name: attachment.name ?? "Unknown", - mimeType: attachment.mimeType); + matrix.MatrixImageFile? thumbnailImageFile; + if (attachment.thumbnailFile != null) { + var decodedImage = await decodeImageFromList(attachment.thumbnailFile!); + + thumbnailImageFile = matrix.MatrixImageFile( + bytes: attachment.thumbnailFile!, + width: decodedImage.width, + height: decodedImage.height, + mimeType: attachment.thumbnailMime, + name: "thumbnail"); + } + + if (Mime.videoTypes.contains(attachment.mimeType)) { + return MatrixProcessedAttachment( + matrix.MatrixVideoFile( + bytes: attachment.data!, + name: attachment.name ?? "Unknown", + mimeType: attachment.mimeType, + width: attachment.dimensions?.width.toInt(), + height: attachment.dimensions?.height.toInt(), + ), + thumbnailFile: thumbnailImageFile, + ); + } + + return MatrixProcessedAttachment( + matrix.MatrixFile( + bytes: attachment.data!, + name: attachment.name ?? "Unknown", + mimeType: attachment.mimeType), + thumbnailFile: thumbnailImageFile); } @override @@ -340,7 +365,8 @@ class MatrixRoom extends Room { processedAttachments.whereType().map((e) { return _matrixRoom.sendFileEvent(e.file, threadLastEventId: threadLastEventId, - threadRootEventId: threadRootEventId); + threadRootEventId: threadRootEventId, + thumbnail: e.thumbnailFile); })); } diff --git a/commet/lib/ui/atoms/message_attachment.dart b/commet/lib/ui/atoms/message_attachment.dart index 6acbf290..7bc6d9b2 100644 --- a/commet/lib/ui/atoms/message_attachment.dart +++ b/commet/lib/ui/atoms/message_attachment.dart @@ -100,6 +100,7 @@ class _MessageAttachmentState extends State { attachment.videoFile, thumbnail: attachment.thumbnail, fileName: attachment.name, + doThumbnail: true, canGoFullscreen: true, onFullscreen: fullscreenVideo, key: videoPlayerKey, diff --git a/commet/lib/ui/molecules/attachment_icon.dart b/commet/lib/ui/molecules/attachment_icon.dart index 49687c14..35bca4f2 100644 --- a/commet/lib/ui/molecules/attachment_icon.dart +++ b/commet/lib/ui/molecules/attachment_icon.dart @@ -16,19 +16,27 @@ class AttachmentIcon extends StatefulWidget { class _AttachmentIconState extends State { bool hovered = false; - @override - Widget build(BuildContext context) { - Image? image; + ImageProvider? image; + @override + void initState() { if (Mime.imageTypes.contains(widget.attachment.mimeType) && widget.attachment.data != null) { - image = Image.memory(widget.attachment.data!, - filterQuality: FilterQuality.medium, fit: BoxFit.cover); + image = Image.memory(widget.attachment.data!).image; + } + + if (widget.attachment.thumbnailFile != null) { + image = Image.memory(widget.attachment.thumbnailFile!).image; } + super.initState(); + } + + @override + Widget build(BuildContext context) { return ImageButton( size: 20, - image: image?.image, + image: image, icon: Mime.toIcon(widget.attachment.mimeType), onTap: widget.removeAttachment, iconSize: 20, diff --git a/commet/lib/ui/molecules/file_preview.dart b/commet/lib/ui/molecules/file_preview.dart index 38a39cce..d5137d2c 100644 --- a/commet/lib/ui/molecules/file_preview.dart +++ b/commet/lib/ui/molecules/file_preview.dart @@ -1,16 +1,26 @@ import 'dart:convert' show utf8; import 'dart:io'; +import 'package:commet/cache/file_provider.dart'; +import 'package:commet/config/platform_utils.dart'; import 'package:commet/ui/atoms/code_block.dart'; +import 'package:commet/ui/molecules/video_player/video_player.dart'; +import 'package:commet/ui/molecules/video_player/video_player_controller.dart'; import 'package:commet/utils/mime.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class FilePreview extends StatefulWidget { - const FilePreview({required this.mimeType, this.path, this.data, super.key}); + const FilePreview( + {required this.mimeType, + this.path, + this.data, + this.videoController, + super.key}); final String? mimeType; final String? path; final Uint8List? data; + final VideoPlayerController? videoController; @override State createState() => _FilePreviewState(); @@ -19,6 +29,8 @@ class FilePreview extends StatefulWidget { class _FilePreviewState extends State { ImageProvider? image; String? text; + FileProvider? videoFile; + GlobalKey videoPlayerKey = GlobalKey(); @override void initState() { @@ -33,6 +45,13 @@ class _FilePreviewState extends State { text = utf8.decode(widget.data!); } } + + if (PlatformUtils.isWeb == false) { + if (Mime.videoTypes.contains(widget.mimeType) && widget.path != null) { + videoFile = SystemFileProvider(File(widget.path!)); + } + } + super.initState(); } @@ -48,6 +67,15 @@ class _FilePreviewState extends State { if (text != null) { return Codeblock(text: text!); } + if (videoFile != null) { + return VideoPlayer( + videoFile!, + decodeFirstFrame: true, + doThumbnail: false, + key: videoPlayerKey, + controller: widget.videoController, + ); + } return const SizedBox( height: 0, ); diff --git a/commet/lib/ui/molecules/video_player/video_player.dart b/commet/lib/ui/molecules/video_player/video_player.dart index d98a08cf..c8ca95b7 100644 --- a/commet/lib/ui/molecules/video_player/video_player.dart +++ b/commet/lib/ui/molecules/video_player/video_player.dart @@ -5,6 +5,7 @@ import 'package:commet/config/build_config.dart'; import 'package:commet/ui/molecules/video_player/video_player_implementation.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:olm/olm.dart'; import 'package:tiamat/tiamat.dart' as tiamat; import '../../atoms/gradient_background.dart'; import 'video_player_controller.dart'; @@ -18,24 +19,30 @@ class VideoPlayer extends StatefulWidget { super.key, this.canGoFullscreen = false, this.onFullscreen, + this.decodeFirstFrame = false, + this.controller, + this.doThumbnail = true, this.showProgressBar = true}); final FileProvider videoFile; final ImageProvider? thumbnail; final bool showProgressBar; final bool canGoFullscreen; + final bool doThumbnail; + final bool decodeFirstFrame; final String? fileName; final Function? onFullscreen; + final VideoPlayerController? controller; @override State createState() => VideoPlayerState(); } class VideoPlayerState extends State { - VideoPlayerController controller = VideoPlayerController(); + late VideoPlayerController controller; bool playing = false; bool inited = false; bool buffering = false; - bool showThumbnail = true; + late bool showThumbnail; bool shouldShowControls = true; bool isCompleted = false; double videoProgress = 0; @@ -48,6 +55,9 @@ class VideoPlayerState extends State { @override void initState() { + showThumbnail = widget.doThumbnail; + + controller = widget.controller ?? VideoPlayerController(); bufferingListener = controller.isBuffering.listen((isBuffering) { setState(() { buffering = isBuffering; @@ -94,7 +104,7 @@ class VideoPlayerState extends State { return Stack( fit: StackFit.expand, children: [ - if (inited) pickPlayer(), + if (widget.decodeFirstFrame || inited) pickPlayer(), if (showThumbnail) thumbnail(), controls() ], @@ -263,6 +273,9 @@ class VideoPlayerState extends State { Widget pickPlayer() { return VideoPlayerImplementation( - controller: controller, videoFile: widget.videoFile); + controller: controller, + videoFile: widget.videoFile, + decodeFirstFrame: widget.decodeFirstFrame, + ); } } diff --git a/commet/lib/ui/molecules/video_player/video_player_controller.dart b/commet/lib/ui/molecules/video_player/video_player_controller.dart index 40fc0119..d83d6418 100644 --- a/commet/lib/ui/molecules/video_player/video_player_controller.dart +++ b/commet/lib/ui/molecules/video_player/video_player_controller.dart @@ -1,4 +1,7 @@ import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; class VideoPlayerController { Future Function()? _onPause; @@ -9,6 +12,10 @@ class VideoPlayerController { Future Function(Duration percent)? _seekTo; + Future Function()? _screenshot; + + Future Function()? _getSize; + Future Function()? _getLength; final StreamController _isBuffering = StreamController.broadcast(); @@ -28,12 +35,16 @@ class VideoPlayerController { required Future Function() play, required Future Function() replay, required Future Function() getLength, + required Future Function() getSize, + Future Function()? screenshot, required Future Function(Duration percent) seekTo}) { _onPause = pause; _onPlay = play; _onReplay = replay; _seekTo = seekTo; _getLength = getLength; + _screenshot = screenshot; + _getSize = getSize; } Future pause() async { @@ -52,6 +63,10 @@ class VideoPlayerController { await _seekTo!.call(duration); } + Future screenshot() async { + return _screenshot?.call(); + } + void setBuffering(bool isBuffering) { _isBuffering.add(isBuffering); } @@ -67,4 +82,8 @@ class VideoPlayerController { Future getLength() async { return await _getLength!.call(); } + + Future getSize() async { + return await _getSize!.call(); + } } diff --git a/commet/lib/ui/molecules/video_player/video_player_implementation.dart b/commet/lib/ui/molecules/video_player/video_player_implementation.dart index 5920a45b..dab55062 100644 --- a/commet/lib/ui/molecules/video_player/video_player_implementation.dart +++ b/commet/lib/ui/molecules/video_player/video_player_implementation.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:commet/cache/file_provider.dart'; import 'package:flutter/widgets.dart'; import 'package:media_kit/media_kit.dart'; @@ -9,12 +11,14 @@ class VideoPlayerImplementation extends StatefulWidget { const VideoPlayerImplementation( {required this.controller, required this.videoFile, + this.decodeFirstFrame = false, this.width = 640, this.height = 340, super.key}); final FileProvider videoFile; final int width; final int height; + final bool decodeFirstFrame; final VideoPlayerController controller; @override State createState() => @@ -37,6 +41,8 @@ class _VideoPlayerImplementationState extends State { pause: pause, play: play, replay: replay, + screenshot: screenshot, + getSize: getSize, seekTo: seekTo, getLength: getLength); @@ -54,7 +60,8 @@ class _VideoPlayerImplementationState extends State { Future.microtask(() async { file = await widget.videoFile.resolve(); - await player.open(Playlist([Media(file.toString())])); + await player.open(Playlist([Media(file.toString())]), + play: !widget.decodeFirstFrame); widget.controller.setBuffering(false); setState(() { @@ -71,7 +78,7 @@ class _VideoPlayerImplementationState extends State { controller: controller!, ); } - return const Placeholder(); + return Container(); } Future pause() async { @@ -82,6 +89,10 @@ class _VideoPlayerImplementationState extends State { player.play(); } + Future screenshot() async { + return player.screenshot(); + } + Future replay() async { await player.open(Playlist([Media(file.toString())])); } @@ -93,4 +104,13 @@ class _VideoPlayerImplementationState extends State { Future getLength() async { return player.state.duration; } + + Future getSize() async { + if (player.state.height == null || player.state.width == null) { + return null; + } + + return Size( + player.state.height!.toDouble(), player.state.width!.toDouble()); + } } diff --git a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart index 4e06607b..878be2eb 100644 --- a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart +++ b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:commet/client/attachment.dart'; import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/ui/molecules/file_preview.dart'; +import 'package:commet/ui/molecules/video_player/video_player_controller.dart'; import 'package:commet/utils/mime.dart'; import 'package:exif/exif.dart'; import 'package:flutter/foundation.dart'; @@ -36,6 +37,7 @@ class _AttachmentProcessorState extends State { Map? exifData; late IconData icon; + VideoPlayerController? videoController; bool canProcessData = false; bool containsGpsData = false; @@ -53,6 +55,9 @@ class _AttachmentProcessorState extends State { if (widget.attachment.mimeType == "image/gif") { canProcessData = false; } + } else if (Mime.videoTypes.contains(widget.attachment.mimeType)) { + videoController = VideoPlayerController(); + canProcessData = true; } super.initState(); } @@ -106,6 +111,7 @@ class _AttachmentProcessorState extends State { mimeType: widget.attachment.mimeType, path: widget.attachment.path, data: widget.attachment.data, + videoController: videoController, ), ), ), @@ -178,6 +184,8 @@ class _AttachmentProcessorState extends State { if (Mime.imageTypes.contains(widget.attachment.mimeType)) { processedFile = await processImage(); + } else if (Mime.videoTypes.contains(widget.attachment.mimeType)) { + processedFile = await processVideo(); } return processedFile; @@ -214,4 +222,20 @@ class _AttachmentProcessorState extends State { mimeType: mime); }, widget.attachment); } + + Future processVideo() async { + var file = widget.attachment; + + if (videoController != null) { + file.thumbnailFile = await videoController!.screenshot(); + if (file.thumbnailFile != null) { + file.thumbnailMime = Mime.lookupType("", data: file.thumbnailFile); + } + } + + file.length = await videoController!.getLength(); + file.dimensions = await videoController!.getSize(); + + return file; + } } From 16348c8155cba5ae94a4c5fe678479479b5cd0f6 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:24:43 +0930 Subject: [PATCH 5/6] fix video thumbnail issue --- .../extensions/matrix_event_extensions.dart | 13 +++++-- .../matrix/matrix_mxc_image_provider.dart | 7 +++- commet/lib/client/matrix/matrix_room.dart | 1 + .../client/matrix/matrix_timeline_event.dart | 35 +++++++++++-------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/commet/lib/client/matrix/extensions/matrix_event_extensions.dart b/commet/lib/client/matrix/extensions/matrix_event_extensions.dart index 1881f449..04a1137d 100644 --- a/commet/lib/client/matrix/extensions/matrix_event_extensions.dart +++ b/commet/lib/client/matrix/extensions/matrix_event_extensions.dart @@ -1,4 +1,5 @@ import 'package:matrix/matrix.dart'; +import 'package:tiamat/config/style/theme_json_converter.dart'; extension MatrixExtensions on Event { String? get attachmentBlurhash => _getBlurhash(); @@ -27,9 +28,17 @@ extension MatrixExtensions on Event { if (info == null) return null; var path = info.tryGet("thumbnail_url") as String?; - if (path == null) return null; + if (path != null) { + return Uri.parse(path); + } - return Uri.parse(path); + var file = info.tryGetMap("thumbnail_file"); + var url = file?.tryGet("url"); + if (url != null) { + return Uri.parse(url); + } + + return null; } double? _attachmentWidth() { diff --git a/commet/lib/client/matrix/matrix_mxc_image_provider.dart b/commet/lib/client/matrix/matrix_mxc_image_provider.dart index fffa3622..62a9d9d1 100644 --- a/commet/lib/client/matrix/matrix_mxc_image_provider.dart +++ b/commet/lib/client/matrix/matrix_mxc_image_provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:commet/main.dart'; +import 'package:commet/utils/mime.dart'; import 'package:matrix/matrix.dart'; import '../../utils/image/lod_image.dart'; @@ -48,7 +49,9 @@ class MatrixMxcImage extends LODImageProvider { if (matrixEvent != null) { var data = await matrixEvent.downloadAndDecryptAttachment(getThumbnail: true); - bytes = data.bytes; + if (Mime.imageTypes.contains(data.mimeType)) { + bytes = data.bytes; + } } else { var response = await client.httpClient .get(uri.getThumbnail(client, width: 90, height: 90)); @@ -80,9 +83,11 @@ class MatrixMxcImage extends LODImageProvider { Uint8List? bytes; if (matrixEvent != null) { var data = await matrixEvent.downloadAndDecryptAttachment(); + bytes = data.bytes; } else { var response = await client.httpClient.get(uri.getDownloadLink(client)); + if (response.statusCode == 200) { bytes = response.bodyBytes; } diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 3e7c4fc6..a57c6691 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -333,6 +333,7 @@ class MatrixRoom extends Room { mimeType: attachment.mimeType, width: attachment.dimensions?.width.toInt(), height: attachment.dimensions?.height.toInt(), + duration: attachment.length?.inMilliseconds, ), thumbnailFile: thumbnailImageFile, ); diff --git a/commet/lib/client/matrix/matrix_timeline_event.dart b/commet/lib/client/matrix/matrix_timeline_event.dart index 6b423faf..60cd3647 100644 --- a/commet/lib/client/matrix/matrix_timeline_event.dart +++ b/commet/lib/client/matrix/matrix_timeline_event.dart @@ -301,6 +301,8 @@ class MatrixTimelineEvent implements TimelineEvent { } void parseAnyAttachments(matrix.Event matrixEvent, matrix.Client client) { + if (matrixEvent.status.isSending) return; + if (matrixEvent.hasAttachment) { double? width = matrixEvent.attachmentWidth; double? height = matrixEvent.attachmentHeight; @@ -319,20 +321,25 @@ class MatrixTimelineEvent implements TimelineEvent { name: matrixEvent.body, height: height); } else if (Mime.videoTypes.contains(matrixEvent.attachmentMimetype)) { - attachment = VideoAttachment( - MxcFileProvider(client, matrixEvent.attachmentMxcUrl!, - event: matrixEvent), - thumbnail: matrixEvent.videoThumbnailUrl != null - ? MatrixMxcImage(matrixEvent.videoThumbnailUrl!, client, - blurhash: matrixEvent.attachmentBlurhash, - doFullres: false, - doThumbnail: true, - matrixEvent: matrixEvent) - : null, - name: matrixEvent.body, - width: width, - fileSize: matrixEvent.infoMap['size'] as int?, - height: height); + // Only load videos if the event has finished sending, otherwise + // matrix dart sdk gives us the video file when we ask for thumbnail + if (matrixEvent.status.isSending == false) { + attachment = VideoAttachment( + MxcFileProvider(client, matrixEvent.attachmentMxcUrl!, + event: matrixEvent), + thumbnail: matrixEvent.videoThumbnailUrl != null + ? MatrixMxcImage(matrixEvent.videoThumbnailUrl!, client, + blurhash: matrixEvent.attachmentBlurhash, + doFullres: false, + autoLoadFullRes: false, + doThumbnail: true, + matrixEvent: matrixEvent) + : null, + name: matrixEvent.body, + width: width, + fileSize: matrixEvent.infoMap['size'] as int?, + height: height); + } } else { attachment = FileAttachment( MxcFileProvider(client, matrixEvent.attachmentMxcUrl!, From 0d6fdaa993ff06aff85c48c598d41ab7124543ea Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:31:48 +0930 Subject: [PATCH 6/6] clean warnings --- commet/lib/client/matrix/extensions/matrix_event_extensions.dart | 1 - commet/lib/ui/molecules/video_player/video_player.dart | 1 - .../ui/organisms/attachment_processor/attachment_processor.dart | 1 - 3 files changed, 3 deletions(-) diff --git a/commet/lib/client/matrix/extensions/matrix_event_extensions.dart b/commet/lib/client/matrix/extensions/matrix_event_extensions.dart index 04a1137d..a476776c 100644 --- a/commet/lib/client/matrix/extensions/matrix_event_extensions.dart +++ b/commet/lib/client/matrix/extensions/matrix_event_extensions.dart @@ -1,5 +1,4 @@ import 'package:matrix/matrix.dart'; -import 'package:tiamat/config/style/theme_json_converter.dart'; extension MatrixExtensions on Event { String? get attachmentBlurhash => _getBlurhash(); diff --git a/commet/lib/ui/molecules/video_player/video_player.dart b/commet/lib/ui/molecules/video_player/video_player.dart index c8ca95b7..639e6d6f 100644 --- a/commet/lib/ui/molecules/video_player/video_player.dart +++ b/commet/lib/ui/molecules/video_player/video_player.dart @@ -5,7 +5,6 @@ import 'package:commet/config/build_config.dart'; import 'package:commet/ui/molecules/video_player/video_player_implementation.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:olm/olm.dart'; import 'package:tiamat/tiamat.dart' as tiamat; import '../../atoms/gradient_background.dart'; import 'video_player_controller.dart'; diff --git a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart index 878be2eb..de9bbd5a 100644 --- a/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart +++ b/commet/lib/ui/organisms/attachment_processor/attachment_processor.dart @@ -8,7 +8,6 @@ import 'package:commet/utils/mime.dart'; import 'package:exif/exif.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; import 'package:image/image.dart' as img;