mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 19:48:39 +00:00
parent
a18d5ab0fd
commit
eb789407d2
18 changed files with 241 additions and 69 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
97
lib/src/services/thumbnail.service.dart
Normal file
97
lib/src/services/thumbnail.service.dart
Normal 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));
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,7 +211,9 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
children: [
|
children: [
|
||||||
icon,
|
icon,
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
Text(
|
(textWidget != null)
|
||||||
|
? textWidget
|
||||||
|
: Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(fontSize: 12),
|
style: TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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++) {
|
||||||
|
|
|
||||||
|
|
@ -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,28 +39,13 @@ 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!)
|
|
||||||
: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
if (_controller != null && _controller!.value.isInitialized)
|
Image.file(widget.galleryItem.thumbnailPath),
|
||||||
Positioned.fill(
|
if (widget.galleryItem.videoPath != null)
|
||||||
child: AspectRatio(
|
Center(
|
||||||
aspectRatio: _controller!.value.aspectRatio,
|
child: FaIcon(FontAwesomeIcons.circlePlay),
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue