This commit is contained in:
otsmr 2025-06-22 22:47:16 +02:00
parent a18d5ab0fd
commit eb789407d2
18 changed files with 241 additions and 69 deletions

View file

@ -224,6 +224,9 @@ PODS:
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- video_thumbnail (0.0.1):
- Flutter
- libwebp
DEPENDENCIES: DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
@ -258,6 +261,7 @@ DEPENDENCIES:
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@ -333,6 +337,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/video_compress/ios" :path: ".symlinks/plugins/video_compress/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
@ -379,6 +385,7 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
PODFILE CHECKSUM: a01f0821a361ca6708e29b1299e8becf492a8a71 PODFILE CHECKSUM: a01f0821a361ca6708e29b1299e8becf492a8a71

View file

@ -242,4 +242,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
..where((t) => ..where((t) =>
t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId)); t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId));
} }
SingleOrNullSelectable<Message> getMessageByIdAndContactId(
int fromUserId, int messageId) {
return select(messages)
..where((t) =>
t.messageId.equals(messageId) & t.contactId.equals(fromUserId));
}
} }

View file

@ -300,5 +300,6 @@
"inviteFriendsShareBtn": "Teilen", "inviteFriendsShareBtn": "Teilen",
"inviteFriendsShareText": "Wechseln wir zu twonly: {url}", "inviteFriendsShareText": "Wechseln wir zu twonly: {url}",
"appOutdated": "Deine Version von twonly ist veraltet.", "appOutdated": "Deine Version von twonly ist veraltet.",
"appOutdatedBtn": "Jetzt aktualisieren." "appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen2": "Doppelklicken zum\nerneuten Öffnen."
} }

View file

@ -457,5 +457,6 @@
"inviteFriendsShareBtn": "Share", "inviteFriendsShareBtn": "Share",
"inviteFriendsShareText": "Let's switch to twonly: {url}", "inviteFriendsShareText": "Let's switch to twonly: {url}",
"appOutdated": "Your version of twonly is out of date.", "appOutdated": "Your version of twonly is out of date.",
"appOutdatedBtn": "Update Now" "appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again"
} }

View file

@ -1843,6 +1843,12 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Update Now'** /// **'Update Now'**
String get appOutdatedBtn; String get appOutdatedBtn;
/// No description provided for @doubleClickToReopen.
///
/// In en, this message translates to:
/// **'Double-click\nto open again'**
String get doubleClickToReopen;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -980,4 +980,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get appOutdatedBtn => 'Jetzt aktualisieren.'; String get appOutdatedBtn => 'Jetzt aktualisieren.';
@override
String get doubleClickToReopen => 'Double-click\nto open again';
} }

View file

@ -974,4 +974,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appOutdatedBtn => 'Update Now'; String get appOutdatedBtn => 'Update Now';
@override
String get doubleClickToReopen => 'Double-click\nto open again';
} }

View file

@ -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/src/services/api/media_upload.dart' as send;
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
class MemoryItem { class MemoryItem {
MemoryItem({ MemoryItem({
@ -12,18 +13,21 @@ class MemoryItem {
required this.messages, required this.messages,
required this.date, required this.date,
required this.mirrorVideo, required this.mirrorVideo,
required this.thumbnailPath,
this.imagePath, this.imagePath,
this.videoPath, this.videoPath,
}); });
final String id; final int id;
final bool mirrorVideo; final bool mirrorVideo;
final List<Message> messages; final List<Message> messages;
final DateTime date; final DateTime date;
final File thumbnailPath;
final File? imagePath; final File? imagePath;
final File? videoPath; final File? videoPath;
static Future<Map<int, MemoryItem>> convertFromMessages( static Future<Map<int, MemoryItem>> convertFromMessages(
List<Message> messages) async { List<Message> messages,
) async {
Map<int, MemoryItem> items = {}; Map<int, MemoryItem> items = {};
for (final message in messages) { for (final message in messages) {
bool isSend = message.messageOtherId == null; bool isSend = message.messageOtherId == null;
@ -33,16 +37,29 @@ class MemoryItem {
isSend ? "send" : "received", isSend ? "send" : "received",
); );
File? imagePath; File? imagePath;
late File thumbnailFile;
File? videoPath; File? videoPath;
if (await File("$basePath.mp4").exists()) { if (await File("$basePath.mp4").exists()) {
videoPath = File("$basePath.mp4"); videoPath = File("$basePath.mp4");
thumbnailFile = getThumbnailPath(videoPath);
if (!await thumbnailFile.exists()) {
await createThumbnailsForVideo(videoPath);
}
} else if (await File("$basePath.png").exists()) { } else if (await File("$basePath.png").exists()) {
imagePath = File("$basePath.png"); imagePath = File("$basePath.png");
thumbnailFile = getThumbnailPath(imagePath);
if (!await thumbnailFile.exists()) {
await createThumbnailsForImage(imagePath);
}
} else { } else {
if (message.mediaStored) { if (message.mediaStored) {
/// media file was deleted, ... remove the file /// media file was deleted, ... remove the file
twonlyDB.messagesDao.updateMessageByMessageId( twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, MessagesCompanion(mediaStored: Value(false))); message.messageId,
MessagesCompanion(
mediaStored: Value(false),
),
);
} }
continue; continue;
} }
@ -57,10 +74,11 @@ class MemoryItem {
.putIfAbsent( .putIfAbsent(
id, id,
() => MemoryItem( () => MemoryItem(
id: id.toString(), id: id,
messages: [], messages: [],
date: message.sendAt, date: message.sendAt,
mirrorVideo: mirrorVideo, mirrorVideo: mirrorVideo,
thumbnailPath: thumbnailFile,
imagePath: imagePath, imagePath: imagePath,
videoPath: videoPath)) videoPath: videoPath))
.messages .messages

View file

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; 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/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.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/encryption.signal.dart';
import 'package:twonly/src/services/signal/identity.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/signal/prekeys.signal.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -249,6 +252,18 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
errorWhileSending: Value(false), 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 { } else {
// when a message is received doubled ignore it... // when a message is received doubled ignore it...
if ((await twonlyDB.messagesDao if ((await twonlyDB.messagesDao

View file

@ -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<void> createThumbnails(String directoryPath) async {
final directory = Directory(directoryPath);
final outputDirectory = await getTemporaryDirectory();
if (await directory.exists()) {
final List<FileSystemEntity> 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));
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -69,6 +70,7 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
if (widget.videoFilePath != null) { if (widget.videoFilePath != null) {
memoryPath += ".mp4"; memoryPath += ".mp4";
await File(widget.videoFilePath!.path).copy(memoryPath); await File(widget.videoFilePath!.path).copy(memoryPath);
createThumbnailsForVideo(File(memoryPath));
if (storeToGallery) { if (storeToGallery) {
res = await saveVideoToGallery(widget.videoFilePath!.path); res = await saveVideoToGallery(widget.videoFilePath!.path);
} }
@ -77,6 +79,7 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
Uint8List? imageBytes = await widget.getMergedImage(); Uint8List? imageBytes = await widget.getMergedImage();
if (imageBytes == null || !mounted) return; if (imageBytes == null || !mounted) return;
await File(memoryPath).writeAsBytes(imageBytes); await File(memoryPath).writeAsBytes(imageBytes);
createThumbnailsForImage(File(memoryPath));
if (storeToGallery) { if (storeToGallery) {
res = await saveImageToGallery(imageBytes); res = await saveImageToGallery(imageBytes);
} }

View file

@ -33,6 +33,7 @@ class ChatMediaEntry extends StatefulWidget {
class _ChatMediaEntryState extends State<ChatMediaEntry> { class _ChatMediaEntryState extends State<ChatMediaEntry> {
GlobalKey reopenMediaFile = GlobalKey(); GlobalKey reopenMediaFile = GlobalKey();
bool canBeReopened = false;
@override @override
void initState() { void initState() {
@ -47,6 +48,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
return; return;
} }
if (await received.existsMediaFile(widget.message.messageId, "png")) { if (await received.existsMediaFile(widget.message.messageId, "png")) {
setState(() {
canBeReopened = true;
});
Future.delayed(Duration(seconds: 1), () { Future.delayed(Duration(seconds: 1), () {
if (!mounted) return; if (!mounted) return;
showReopenMediaFilesTutorial(context, reopenMediaFile); showReopenMediaFilesTutorial(context, reopenMediaFile);
@ -119,6 +123,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
contact: widget.contact, contact: widget.contact,
color: color, color: color,
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
canBeReopened: canBeReopened,
), ),
), ),
), ),

View file

@ -20,12 +20,14 @@ class InChatMediaViewer extends StatefulWidget {
required this.contact, required this.contact,
required this.color, required this.color,
required this.galleryItems, required this.galleryItems,
required this.canBeReopened,
}); });
final Message message; final Message message;
final Contact contact; final Contact contact;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final Color color; final Color color;
final bool canBeReopened;
@override @override
State<InChatMediaViewer> createState() => _InChatMediaViewerState(); State<InChatMediaViewer> createState() => _InChatMediaViewerState();
@ -121,35 +123,20 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
initialIndex: widget.galleryItems.indexWhere((x) => initialIndex: widget.galleryItems.indexWhere((x) =>
x.id == x.id ==
(widget.message.mediaUploadId ?? widget.message.messageId) (widget.message.mediaUploadId ?? widget.message.messageId)),
.toString()),
scrollDirection: Axis.horizontal, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (image == null && video == null) { if (image == null && video == null) {
return Container( return Container(
constraints: BoxConstraints(
minHeight: 39,
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: widget.color, color: widget.color,
@ -158,10 +145,12 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(10.0), padding: EdgeInsets.symmetric(
vertical: (widget.canBeReopened) ? 5 : 10.0, horizontal: 4),
child: MessageSendStateIcon( child: MessageSendStateIcon(
[widget.message], [widget.message],
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
canBeReopened: widget.canBeReopened,
), ),
), ),
); );

View file

@ -49,9 +49,14 @@ MessageSendState messageSendStateFromMessage(Message msg) {
class MessageSendStateIcon extends StatefulWidget { class MessageSendStateIcon extends StatefulWidget {
final List<Message> messages; final List<Message> messages;
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
final bool canBeReopened;
const MessageSendStateIcon(this.messages, const MessageSendStateIcon(
{super.key, this.mainAxisAlignment = MainAxisAlignment.end}); this.messages, {
super.key,
this.canBeReopened = false,
this.mainAxisAlignment = MainAxisAlignment.end,
});
@override @override
State<MessageSendStateIcon> createState() => _MessageSendStateIconState(); State<MessageSendStateIcon> createState() => _MessageSendStateIconState();
@ -82,6 +87,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
String text = ""; String text = "";
HashSet<MessageKind> kindsAlreadyShown = HashSet(); HashSet<MessageKind> kindsAlreadyShown = HashSet();
Widget? textWidget;
for (final message in widget.messages) { for (final message in widget.messages) {
if (icons.length == 2) break; if (icons.length == 2) break;
@ -104,6 +110,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
Widget icon = Placeholder(); Widget icon = Placeholder();
textWidget = null;
switch (state) { switch (state) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
@ -114,6 +121,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
} }
text = context.lang.messageSendState_Received; text = context.lang.messageSendState_Received;
if (widget.canBeReopened) {
textWidget = Text(
context.lang.doubleClickToReopen,
style: TextStyle(fontSize: 9),
);
}
break; break;
case MessageSendState.sendOpened: case MessageSendState.sendOpened:
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color); icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
@ -198,10 +211,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
children: [ children: [
icon, icon,
const SizedBox(width: 3), const SizedBox(width: 3),
Text( (textWidget != null)
text, ? textWidget
style: TextStyle(fontSize: 12), : Text(
), text,
style: TextStyle(fontSize: 12),
),
const SizedBox(width: 5), const SizedBox(width: 5),
], ],
); );

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/memory_item.model.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/utils/misc.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
@ -49,19 +50,32 @@ class MemoriesViewState extends State<MemoriesView> {
final fileName = file.uri.pathSegments.last; final fileName = file.uri.pathSegments.last;
File? imagePath; File? imagePath;
File? videoPath; File? videoPath;
late File thumbnailFile;
if (fileName.contains(".thumbnail.")) {
continue;
}
if (fileName.contains(".png")) { if (fileName.contains(".png")) {
imagePath = file; imagePath = file;
thumbnailFile = getThumbnailPath(imagePath);
if (!await thumbnailFile.exists()) {
await createThumbnailsForImage(imagePath);
}
} else if (fileName.contains(".mp4")) { } else if (fileName.contains(".mp4")) {
videoPath = file; videoPath = file;
thumbnailFile = getThumbnailPath(videoPath);
if (!await thumbnailFile.exists()) {
await createThumbnailsForVideo(videoPath);
}
} else { } else {
break; break;
} }
final creationDate = await file.lastModified(); final creationDate = await file.lastModified();
items.add(MemoryItem( items.add(MemoryItem(
id: fileName, id: int.parse(fileName.split(".")[0]),
messages: [], messages: [],
date: creationDate, date: creationDate,
mirrorVideo: false, mirrorVideo: false,
thumbnailPath: thumbnailFile,
imagePath: imagePath, imagePath: imagePath,
videoPath: videoPath, videoPath: videoPath,
)); ));
@ -83,6 +97,10 @@ class MemoriesViewState extends State<MemoriesView> {
months = []; months = [];
String lastMonth = ""; String lastMonth = "";
galleryItems = await loadMemoriesDirectory(); 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 += items.values.toList();
galleryItems.sort((a, b) => b.date.compareTo(a.date)); galleryItems.sort((a, b) => b.date.compareTo(a.date));
for (var i = 0; i < galleryItems.length; i++) { for (var i = 0; i < galleryItems.length; i++) {

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; 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:twonly/src/model/memory_item.model.dart';
import 'package:video_player/video_player.dart';
class MemoriesItemThumbnail extends StatefulWidget { class MemoriesItemThumbnail extends StatefulWidget {
const MemoriesItemThumbnail({ const MemoriesItemThumbnail({
@ -17,23 +17,13 @@ class MemoriesItemThumbnail extends StatefulWidget {
} }
class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> { class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
VideoPlayerController? _controller;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.galleryItem.videoPath != null) {
_controller = VideoPlayerController.file(widget.galleryItem.videoPath!)
..initialize().then((_) {
setState(() {});
});
}
} }
@override @override
void dispose() { void dispose() {
_controller?.dispose();
super.dispose(); super.dispose();
} }
@ -49,31 +39,16 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Hero( child: Hero(
tag: widget.galleryItem.id, tag: widget.galleryItem.id.toString(),
child: (widget.galleryItem.imagePath != null) child: Stack(
? Image.file(widget.galleryItem.imagePath!) children: [
: Stack( Image.file(widget.galleryItem.thumbnailPath),
children: [ if (widget.galleryItem.videoPath != null)
if (_controller != null && _controller!.value.isInitialized) Center(
Positioned.fill( child: FaIcon(FontAwesomeIcons.circlePlay),
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),
),
)
],
),
), ),
); );
} }

View file

@ -1824,6 +1824,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" 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: vm_service:
dependency: transitive dependency: transitive
description: description:

View file

@ -67,6 +67,7 @@ dependencies:
tutorial_coach_mark: ^1.3.0 tutorial_coach_mark: ^1.3.0
background_downloader: ^9.2.2 background_downloader: ^9.2.2
hashlib: ^2.0.0 hashlib: ^2.0.0
video_thumbnail: ^0.5.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: