From eb789407d2a3bf71d64da3b957df87fc636a98f1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 22 Jun 2025 22:47:16 +0200 Subject: [PATCH] fix #209 #206 --- ios/Podfile.lock | 7 ++ lib/src/database/daos/messages_dao.dart | 7 ++ lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 ++ .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + lib/src/model/memory_item.model.dart | 26 ++++- lib/src/services/api/server_messages.dart | 15 +++ lib/src/services/thumbnail.service.dart | 97 +++++++++++++++++++ .../save_to_gallery.dart | 3 + .../chat_media_entry.dart | 5 + .../in_chat_media_viewer.dart | 29 ++---- .../components/message_send_state_icon.dart | 27 ++++-- lib/src/views/memories/memories.view.dart | 20 +++- .../memories/memories_item_thumbnail.dart | 47 +++------ pubspec.lock | 8 ++ pubspec.yaml | 1 + 18 files changed, 241 insertions(+), 69 deletions(-) create mode 100644 lib/src/services/thumbnail.service.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cca8a12..5aa465a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -224,6 +224,9 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS + - video_thumbnail (0.0.1): + - Flutter + - libwebp DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) @@ -258,6 +261,7 @@ DEPENDENCIES: - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) SPEC REPOS: trunk: @@ -333,6 +337,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_compress/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" + video_thumbnail: + :path: ".symlinks/plugins/video_thumbnail/ios" SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad @@ -379,6 +385,7 @@ SPEC CHECKSUMS: url_launcher_ios: 694010445543906933d732453a59da0a173ae33d video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 PODFILE CHECKSUM: a01f0821a361ca6708e29b1299e8becf492a8a71 diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index 68c8dc8..4e916e4 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -242,4 +242,11 @@ class MessagesDao extends DatabaseAccessor ..where((t) => t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId)); } + + SingleOrNullSelectable getMessageByIdAndContactId( + int fromUserId, int messageId) { + return select(messages) + ..where((t) => + t.messageId.equals(messageId) & t.contactId.equals(fromUserId)); + } } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index af7671e..e5d9d8c 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -300,5 +300,6 @@ "inviteFriendsShareBtn": "Teilen", "inviteFriendsShareText": "Wechseln wir zu twonly: {url}", "appOutdated": "Deine Version von twonly ist veraltet.", - "appOutdatedBtn": "Jetzt aktualisieren." + "appOutdatedBtn": "Jetzt aktualisieren.", + "doubleClickToReopen2": "Doppelklicken zum\nerneuten Öffnen." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 97e7246..5f78766 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -457,5 +457,6 @@ "inviteFriendsShareBtn": "Share", "inviteFriendsShareText": "Let's switch to twonly: {url}", "appOutdated": "Your version of twonly is out of date.", - "appOutdatedBtn": "Update Now" + "appOutdatedBtn": "Update Now", + "doubleClickToReopen": "Double-click\nto open again" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 56127d5..13ea3a5 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1843,6 +1843,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Update Now'** String get appOutdatedBtn; + + /// No description provided for @doubleClickToReopen. + /// + /// In en, this message translates to: + /// **'Double-click\nto open again'** + String get doubleClickToReopen; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index d3989ea..2b01246 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -980,4 +980,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appOutdatedBtn => 'Jetzt aktualisieren.'; + + @override + String get doubleClickToReopen => 'Double-click\nto open again'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 73ccca7..9937b15 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -974,4 +974,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appOutdatedBtn => 'Update Now'; + + @override + String get doubleClickToReopen => 'Double-click\nto open again'; } diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index ad34245..e05f78b 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -5,6 +5,7 @@ import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/services/api/media_upload.dart' as send; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; class MemoryItem { MemoryItem({ @@ -12,18 +13,21 @@ class MemoryItem { required this.messages, required this.date, required this.mirrorVideo, + required this.thumbnailPath, this.imagePath, this.videoPath, }); - final String id; + final int id; final bool mirrorVideo; final List messages; final DateTime date; + final File thumbnailPath; final File? imagePath; final File? videoPath; static Future> convertFromMessages( - List messages) async { + List messages, + ) async { Map items = {}; for (final message in messages) { bool isSend = message.messageOtherId == null; @@ -33,16 +37,29 @@ class MemoryItem { isSend ? "send" : "received", ); File? imagePath; + late File thumbnailFile; File? videoPath; if (await File("$basePath.mp4").exists()) { videoPath = File("$basePath.mp4"); + thumbnailFile = getThumbnailPath(videoPath); + if (!await thumbnailFile.exists()) { + await createThumbnailsForVideo(videoPath); + } } else if (await File("$basePath.png").exists()) { imagePath = File("$basePath.png"); + thumbnailFile = getThumbnailPath(imagePath); + if (!await thumbnailFile.exists()) { + await createThumbnailsForImage(imagePath); + } } else { if (message.mediaStored) { /// media file was deleted, ... remove the file twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, MessagesCompanion(mediaStored: Value(false))); + message.messageId, + MessagesCompanion( + mediaStored: Value(false), + ), + ); } continue; } @@ -57,10 +74,11 @@ class MemoryItem { .putIfAbsent( id, () => MemoryItem( - id: id.toString(), + id: id, messages: [], date: message.sendAt, mirrorVideo: mirrorVideo, + thumbnailPath: thumbnailFile, imagePath: imagePath, videoPath: videoPath)) .messages diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 7ecec2e..c547c19 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; @@ -13,6 +14,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; +import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/media_download.dart'; @@ -21,6 +23,7 @@ import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -249,6 +252,18 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { errorWhileSending: Value(false), ), ); + final message = await twonlyDB.messagesDao + .getMessageByIdAndContactId(fromUserId, content.messageId) + .getSingleOrNull(); + if (message != null && message.mediaUploadId != null) { + final filePath = + await getMediaFilePath(message.mediaUploadId, "send"); + if (filePath.contains("mp4")) { + createThumbnailsForVideo(File(filePath)); + } else { + createThumbnailsForImage(File(filePath)); + } + } } else { // when a message is received doubled ignore it... if ((await twonlyDB.messagesDao diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/thumbnail.service.dart new file mode 100644 index 0000000..227b20e --- /dev/null +++ b/lib/src/services/thumbnail.service.dart @@ -0,0 +1,97 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path/path.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; +import 'package:path_provider/path_provider.dart'; + +Future createThumbnails(String directoryPath) async { + final directory = Directory(directoryPath); + final outputDirectory = await getTemporaryDirectory(); + + if (await directory.exists()) { + final List files = directory.listSync(); + + for (var file in files) { + if (file is File) { + final String filePath = file.path; + final String fileExtension = filePath.split('.').last.toLowerCase(); + + if (['jpg', 'jpeg', 'png'].contains(fileExtension)) { + // Create thumbnail for images + final image = await decodeImageFromList(file.readAsBytesSync()); + final thumbnail = await image.toByteData(format: ImageByteFormat.png); + final thumbnailFile = + File('${outputDirectory.path}/${file.uri.pathSegments.last}'); + await thumbnailFile.writeAsBytes(thumbnail!.buffer.asUint8List()); + print('Thumbnail created for image: ${file.uri.pathSegments.last}'); + } else if (['mp4', 'mov', 'avi'].contains(fileExtension)) { + // Create thumbnail for videos + + print('Thumbnail created for video: ${file.uri.pathSegments.last}'); + } + } + } + } else { + print('Directory does not exist: $directoryPath'); + } +} + +Future createThumbnailsForImage(File file) async { + final String fileExtension = file.path.split('.').last.toLowerCase(); + if (fileExtension != "png") { + Log.error("Could not create thumbnail for image. $fileExtension != .png"); + return; + } + + try { + final imageBytesCompressed = await FlutterImageCompress.compressWithFile( + minHeight: 800, + minWidth: 450, + file.path, + format: CompressFormat.png, + quality: 30, + ); + + if (imageBytesCompressed == null) { + Log.error("Could not compress the image"); + return; + } + + File thumbnailFile = getThumbnailPath(file); + await thumbnailFile.writeAsBytes(imageBytesCompressed); + } catch (e) { + Log.error("Could not compress the image got :$e"); + } +} + +Future createThumbnailsForVideo(File file) async { + final String fileExtension = file.path.split('.').last.toLowerCase(); + if (fileExtension != "mp4") { + Log.error("Could not create thumbnail for video. $fileExtension != .mp4"); + return; + } + + try { + String? thumbnailFile = await VideoThumbnail.thumbnailFile( + video: file.path, + imageFormat: ImageFormat.PNG, + maxWidth: 450, + quality: 75, + ); + + File(thumbnailFile!).rename(getThumbnailPath(file).path); + } catch (e) { + Log.error("Could not create the video thumbnail: $e"); + } +} + +File getThumbnailPath(File file) { + String originalFileName = file.uri.pathSegments.last; + String fileNameWithoutExtension = originalFileName.split('.').first; + String fileExtension = originalFileName.split('.').last; + String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension'; + return File(join(file.parent.path, newFileName)); +} diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 3cc1f7f..e4cc35f 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:path/path.dart'; import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; import 'dart:typed_data'; import 'package:twonly/src/utils/misc.dart'; @@ -69,6 +70,7 @@ class SaveToGalleryButtonState extends State { if (widget.videoFilePath != null) { memoryPath += ".mp4"; await File(widget.videoFilePath!.path).copy(memoryPath); + createThumbnailsForVideo(File(memoryPath)); if (storeToGallery) { res = await saveVideoToGallery(widget.videoFilePath!.path); } @@ -77,6 +79,7 @@ class SaveToGalleryButtonState extends State { Uint8List? imageBytes = await widget.getMergedImage(); if (imageBytes == null || !mounted) return; await File(memoryPath).writeAsBytes(imageBytes); + createThumbnailsForImage(File(memoryPath)); if (storeToGallery) { res = await saveImageToGallery(imageBytes); } diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 8e46bc5..95e9477 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -33,6 +33,7 @@ class ChatMediaEntry extends StatefulWidget { class _ChatMediaEntryState extends State { GlobalKey reopenMediaFile = GlobalKey(); + bool canBeReopened = false; @override void initState() { @@ -47,6 +48,9 @@ class _ChatMediaEntryState extends State { return; } if (await received.existsMediaFile(widget.message.messageId, "png")) { + setState(() { + canBeReopened = true; + }); Future.delayed(Duration(seconds: 1), () { if (!mounted) return; showReopenMediaFilesTutorial(context, reopenMediaFile); @@ -119,6 +123,7 @@ class _ChatMediaEntryState extends State { contact: widget.contact, color: color, galleryItems: widget.galleryItems, + canBeReopened: canBeReopened, ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index fdc5c8b..47c8d74 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -20,12 +20,14 @@ class InChatMediaViewer extends StatefulWidget { required this.contact, required this.color, required this.galleryItems, + required this.canBeReopened, }); final Message message; final Contact contact; final List galleryItems; final Color color; + final bool canBeReopened; @override State createState() => _InChatMediaViewerState(); @@ -121,35 +123,20 @@ class _InChatMediaViewerState extends State { galleryItems: widget.galleryItems, initialIndex: widget.galleryItems.indexWhere((x) => x.id == - (widget.message.mediaUploadId ?? widget.message.messageId) - .toString()), + (widget.message.mediaUploadId ?? widget.message.messageId)), scrollDirection: Axis.horizontal, ), ), ); - // bool? removed = await Navigator.push( - // context, - // MaterialPageRoute(builder: (context) { - // return ChatMediaViewerFullScreen( - // message: widget.message, - // contact: widget.contact, - // color: widget.color, - // ); - // }), - // ); - - // if (removed != null && removed) { - // image = null; - // videoController?.dispose(); - // videoController = null; - // if (isMounted) setState(() {}); - // } } @override Widget build(BuildContext context) { if (image == null && video == null) { return Container( + constraints: BoxConstraints( + minHeight: 39, + ), decoration: BoxDecoration( border: Border.all( color: widget.color, @@ -158,10 +145,12 @@ class _InChatMediaViewerState extends State { borderRadius: BorderRadius.circular(12.0), ), child: Padding( - padding: const EdgeInsets.all(10.0), + padding: EdgeInsets.symmetric( + vertical: (widget.canBeReopened) ? 5 : 10.0, horizontal: 4), child: MessageSendStateIcon( [widget.message], mainAxisAlignment: MainAxisAlignment.center, + canBeReopened: widget.canBeReopened, ), ), ); diff --git a/lib/src/views/components/message_send_state_icon.dart b/lib/src/views/components/message_send_state_icon.dart index 9dc4db7..c78470c 100644 --- a/lib/src/views/components/message_send_state_icon.dart +++ b/lib/src/views/components/message_send_state_icon.dart @@ -49,9 +49,14 @@ MessageSendState messageSendStateFromMessage(Message msg) { class MessageSendStateIcon extends StatefulWidget { final List messages; final MainAxisAlignment mainAxisAlignment; + final bool canBeReopened; - const MessageSendStateIcon(this.messages, - {super.key, this.mainAxisAlignment = MainAxisAlignment.end}); + const MessageSendStateIcon( + this.messages, { + super.key, + this.canBeReopened = false, + this.mainAxisAlignment = MainAxisAlignment.end, + }); @override State createState() => _MessageSendStateIconState(); @@ -82,6 +87,7 @@ class _MessageSendStateIconState extends State { String text = ""; HashSet kindsAlreadyShown = HashSet(); + Widget? textWidget; for (final message in widget.messages) { if (icons.length == 2) break; @@ -104,6 +110,7 @@ class _MessageSendStateIconState extends State { } Widget icon = Placeholder(); + textWidget = null; switch (state) { case MessageSendState.receivedOpened: @@ -114,6 +121,12 @@ class _MessageSendStateIconState extends State { } } text = context.lang.messageSendState_Received; + if (widget.canBeReopened) { + textWidget = Text( + context.lang.doubleClickToReopen, + style: TextStyle(fontSize: 9), + ); + } break; case MessageSendState.sendOpened: icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color); @@ -198,10 +211,12 @@ class _MessageSendStateIconState extends State { children: [ icon, const SizedBox(width: 3), - Text( - text, - style: TextStyle(fontSize: 12), - ), + (textWidget != null) + ? textWidget + : Text( + text, + style: TextStyle(fontSize: 12), + ), const SizedBox(width: 5), ], ); diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index e0d223c..f403858 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -49,19 +50,32 @@ class MemoriesViewState extends State { final fileName = file.uri.pathSegments.last; File? imagePath; File? videoPath; + late File thumbnailFile; + if (fileName.contains(".thumbnail.")) { + continue; + } if (fileName.contains(".png")) { imagePath = file; + thumbnailFile = getThumbnailPath(imagePath); + if (!await thumbnailFile.exists()) { + await createThumbnailsForImage(imagePath); + } } else if (fileName.contains(".mp4")) { videoPath = file; + thumbnailFile = getThumbnailPath(videoPath); + if (!await thumbnailFile.exists()) { + await createThumbnailsForVideo(videoPath); + } } else { break; } final creationDate = await file.lastModified(); items.add(MemoryItem( - id: fileName, + id: int.parse(fileName.split(".")[0]), messages: [], date: creationDate, mirrorVideo: false, + thumbnailPath: thumbnailFile, imagePath: imagePath, videoPath: videoPath, )); @@ -83,6 +97,10 @@ class MemoriesViewState extends State { months = []; String lastMonth = ""; galleryItems = await loadMemoriesDirectory(); + for (final item in galleryItems) { + items.remove(item + .id); // prefer the stored one and not the saved on in the chat.... + } galleryItems += items.values.toList(); galleryItems.sort((a, b) => b.date.compareTo(a.date)); for (var i = 0; i < galleryItems.length; i++) { diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index afdce4b..6e948fd 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:video_player/video_player.dart'; class MemoriesItemThumbnail extends StatefulWidget { const MemoriesItemThumbnail({ @@ -17,23 +17,13 @@ class MemoriesItemThumbnail extends StatefulWidget { } class _MemoriesItemThumbnailState extends State { - VideoPlayerController? _controller; - @override void initState() { super.initState(); - - if (widget.galleryItem.videoPath != null) { - _controller = VideoPlayerController.file(widget.galleryItem.videoPath!) - ..initialize().then((_) { - setState(() {}); - }); - } } @override void dispose() { - _controller?.dispose(); super.dispose(); } @@ -49,31 +39,16 @@ class _MemoriesItemThumbnailState extends State { return GestureDetector( onTap: widget.onTap, child: Hero( - tag: widget.galleryItem.id, - child: (widget.galleryItem.imagePath != null) - ? Image.file(widget.galleryItem.imagePath!) - : Stack( - children: [ - if (_controller != null && _controller!.value.isInitialized) - Positioned.fill( - child: AspectRatio( - aspectRatio: _controller!.value.aspectRatio, - child: VideoPlayer(_controller!), - )), - if (_controller != null && _controller!.value.isInitialized) - Positioned( - bottom: 5, - right: 5, - child: Text( - _controller!.value.isInitialized - ? formatDuration(_controller!.value.duration) - : '...', - style: TextStyle( - fontSize: 15, fontWeight: FontWeight.bold), - ), - ) - ], - ), + tag: widget.galleryItem.id.toString(), + child: Stack( + children: [ + Image.file(widget.galleryItem.thumbnailPath), + if (widget.galleryItem.videoPath != null) + Center( + child: FaIcon(FontAwesomeIcons.circlePlay), + ) + ], + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index cb53cba..d38fe62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1824,6 +1824,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.5" + video_thumbnail: + dependency: "direct main" + description: + name: video_thumbnail + sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" + url: "https://pub.dev" + source: hosted + version: "0.5.6" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc95b72..a35fc73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: tutorial_coach_mark: ^1.3.0 background_downloader: ^9.2.2 hashlib: ^2.0.0 + video_thumbnail: ^0.5.6 dev_dependencies: flutter_test: