From b84eba59e35662aba9cce0c19abcc1bd7ef9b6ed Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 12 Mar 2021 19:36:42 +0100 Subject: [PATCH] Generate video thumbnails --- lib/helpers/conversations_helper.dart | 3 ++ lib/helpers/server_config_helper.dart | 10 ++++-- lib/models/new_conversation_message.dart | 4 +++ lib/models/server_config.dart | 14 +++++++- lib/ui/dialogs/pick_file_dialog.dart | 8 ++--- lib/ui/screens/conversation_screen.dart | 19 +++++++--- lib/ui/tiles/conversation_message_tile.dart | 4 ++- lib/utils/video_utils.dart | 40 +++++++++++++++++++++ pubspec.lock | 7 ++++ pubspec.yaml | 3 ++ 10 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 lib/utils/video_utils.dart diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart index 054c4c7..0916cdb 100644 --- a/lib/helpers/conversations_helper.dart +++ b/lib/helpers/conversations_helper.dart @@ -312,6 +312,9 @@ class ConversationsHelper { // Check for file if (message.hasFile) request.addBytesFile("file", message.file); + if (message.hasThumbnail) + request.addBytesFile("thumbnail", message.thumbnail); + //Send the message APIResponse response; if (!message.hasFile) diff --git a/lib/helpers/server_config_helper.dart b/lib/helpers/server_config_helper.dart index 195706d..6b8ee9f 100644 --- a/lib/helpers/server_config_helper.dart +++ b/lib/helpers/server_config_helper.dart @@ -54,10 +54,16 @@ class ServerConfigurationHelper { conversationsPolicy: ConversationsPolicy( minMessageLen: conversationsPolicy["min_message_len"], maxMessageLen: conversationsPolicy["max_message_len"], - allowedFilesType: conversationsPolicy["allowed_files_type"].cast(), + allowedFilesType: + conversationsPolicy["allowed_files_type"].cast(), filesMaxSize: conversationsPolicy["files_max_size"], writingEventInterval: conversationsPolicy["writing_event_interval"], writingEventLifetime: conversationsPolicy["writing_event_lifetime"], + maxMessageImageWidth: conversationsPolicy["max_message_image_width"], + maxMessageImageHeight: + conversationsPolicy["max_message_image_height"], + maxThumbnailWidth: conversationsPolicy["max_thumbnail_width"], + maxThumbnailHeight: conversationsPolicy["max_thumbnail_height"], )); } @@ -72,4 +78,4 @@ class ServerConfigurationHelper { } /// Shortcut for server configuration -ServerConfig get srvConfig => ServerConfigurationHelper.config; \ No newline at end of file +ServerConfig get srvConfig => ServerConfigurationHelper.config; diff --git a/lib/models/new_conversation_message.dart b/lib/models/new_conversation_message.dart index 9fa8b8d..c4264a3 100644 --- a/lib/models/new_conversation_message.dart +++ b/lib/models/new_conversation_message.dart @@ -11,15 +11,19 @@ class NewConversationMessage { final int conversationID; final String message; final BytesFile file; + final BytesFile thumbnail; NewConversationMessage({ @required this.conversationID, @required this.message, this.file, + this.thumbnail, }) : assert(conversationID != null), assert(file != null || message != null); bool get hasMessage => message != null; bool get hasFile => file != null; + + bool get hasThumbnail => thumbnail != null; } diff --git a/lib/models/server_config.dart b/lib/models/server_config.dart index 5e48390..f2301f5 100644 --- a/lib/models/server_config.dart +++ b/lib/models/server_config.dart @@ -64,6 +64,10 @@ class ConversationsPolicy { final int filesMaxSize; final int writingEventInterval; final int writingEventLifetime; + final int maxMessageImageWidth; + final int maxMessageImageHeight; + final int maxThumbnailWidth; + final int maxThumbnailHeight; const ConversationsPolicy({ @required this.minMessageLen, @@ -72,12 +76,20 @@ class ConversationsPolicy { @required this.filesMaxSize, @required this.writingEventInterval, @required this.writingEventLifetime, + @required this.maxMessageImageWidth, + @required this.maxMessageImageHeight, + @required this.maxThumbnailWidth, + @required this.maxThumbnailHeight, }) : assert(minMessageLen != null), assert(maxMessageLen != null), assert(allowedFilesType != null), assert(filesMaxSize != null), assert(writingEventInterval != null), - assert(writingEventLifetime != null); + assert(writingEventLifetime != null), + assert(maxMessageImageWidth != null), + assert(maxMessageImageHeight != null), + assert(maxThumbnailWidth != null), + assert(maxThumbnailHeight != null); } class ServerConfig { diff --git a/lib/ui/dialogs/pick_file_dialog.dart b/lib/ui/dialogs/pick_file_dialog.dart index 0369feb..b6a1a6f 100644 --- a/lib/ui/dialogs/pick_file_dialog.dart +++ b/lib/ui/dialogs/pick_file_dialog.dart @@ -77,8 +77,8 @@ Future showPickFileDialog({ @required BuildContext context, int maxFileSize, List allowedMimeTypes, - double imageMaxWidth, - double imageMaxHeight, + int imageMaxWidth, + int imageMaxHeight, }) async { assert(allowedMimeTypes != null); @@ -113,8 +113,8 @@ Future showPickFileDialog({ source: choice == _FileChoices.PICK_IMAGE ? ImageSource.gallery : ImageSource.camera, - maxWidth: imageMaxWidth, - maxHeight: imageMaxHeight, + maxWidth: imageMaxWidth.toDouble(), + maxHeight: imageMaxHeight.toDouble(), ); if (image == null) return null; diff --git a/lib/ui/screens/conversation_screen.dart b/lib/ui/screens/conversation_screen.dart index 8a4b86e..ec34df0 100644 --- a/lib/ui/screens/conversation_screen.dart +++ b/lib/ui/screens/conversation_screen.dart @@ -6,6 +6,7 @@ import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/conversation_messages_list.dart'; import 'package:comunic/lists/users_list.dart'; +import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/new_conversation_message.dart'; @@ -15,11 +16,14 @@ import 'package:comunic/ui/tiles/conversation_message_tile.dart'; import 'package:comunic/ui/tiles/server_conversation_message_tile.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/ui/widgets/scroll_watcher.dart'; +import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/list_utils.dart'; import 'package:comunic/utils/log_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; +import 'package:comunic/utils/video_utils.dart'; import 'package:flutter/material.dart'; +import 'package:mime/mime.dart'; /// Conversation screen /// @@ -227,16 +231,23 @@ class _ConversationScreenState extends SafeState { context: context, maxFileSize: srvConfig.conversationsPolicy.filesMaxSize, allowedMimeTypes: srvConfig.conversationsPolicy.allowedFilesType, + imageMaxWidth: srvConfig.conversationsPolicy.maxMessageImageWidth, + imageMaxHeight: srvConfig.conversationsPolicy.maxMessageImageHeight, ); if (file == null) return; + BytesFile thumbnail; + + if (isVideo(lookupMimeType(file.filename))) + thumbnail = await generateVideoThumbnail(videoFile: file); + await _submitMessage( NewConversationMessage( - conversationID: widget.conversationID, - message: null, - file: file, - ), + conversationID: widget.conversationID, + message: null, + file: file, + thumbnail: thumbnail), ); } catch (e, s) { logError(e, s); diff --git a/lib/ui/tiles/conversation_message_tile.dart b/lib/ui/tiles/conversation_message_tile.dart index d1da64d..1b0cb9b 100644 --- a/lib/ui/tiles/conversation_message_tile.dart +++ b/lib/ui/tiles/conversation_message_tile.dart @@ -90,7 +90,9 @@ class ConversationMessageTile extends StatelessWidget { // Update message content PopupMenuItem( - enabled: message.isOwner, + enabled: message.isOwner && + message.message != null && + message.message.content.isNotEmpty, value: _MenuChoices.REQUEST_UPDATE_CONTENT, child: Text(tr("Update")), ), diff --git a/lib/utils/video_utils.dart b/lib/utils/video_utils.dart new file mode 100644 index 0000000..23e86dd --- /dev/null +++ b/lib/utils/video_utils.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:comunic/models/api_request.dart'; +import 'package:comunic/utils/log_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:random_string/random_string.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; + +/// Video utilities +/// +/// @author Pierre Hubert + +/// Generate a thumbnail for a video. In case of failure, return null +Future generateVideoThumbnail({ + @required BytesFile videoFile, + int maxWidth, +}) async { + File file; + + try { + final tempDir = await getTemporaryDirectory(); + if (tempDir == null) return null; + file = File(path.join(tempDir.path, randomString(15, from: 65, to: 90))); + + await file.writeAsBytes(videoFile.bytes); + + return BytesFile( + "thumb.png", + await VideoThumbnail.thumbnailData( + video: file.absolute.path, maxWidth: maxWidth), + ); + } catch (e, s) { + logError(e, s); + return null; + } finally { + if (file != null && await file.exists()) await file.delete(); + } +} diff --git a/pubspec.lock b/pubspec.lock index b9f3175..c469fa2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -686,6 +686,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4+1" + video_thumbnail: + dependency: "direct main" + description: + name: video_thumbnail + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5+1" wakelock: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 20c5ee9..ff5458b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,9 @@ dependencies: # Determine file mime type mime: ^0.9.7 + # Create video thumbnails + video_thumbnail: ^0.2.5+1 + dev_dependencies: flutter_test: sdk: flutter