Compare commits

...

3 Commits

11 changed files with 167 additions and 48 deletions

View File

@ -445,8 +445,23 @@ class HttpFileTransferService {
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();
} else if (mime.startsWith('video/')) {
// TODO(Unknown): Also figure out the thumbnail size here
MoxplatformPlugin.media.scanFile(downloadedPath);
/*
// Generate thumbnail
final thumbnailPath = await getVideoThumbnailPath(
downloadedPath,
job.conversationJid,
);
// Find out the dimensions
final imageSize = await getImageSizeFromPath(thumbnailPath);
if (imageSize == null) {
_log.warning('Failed to get image size for $downloadedPath ($thumbnailPath)');
}
mediaWidth = imageSize?.width.toInt();
mediaHeight = imageSize?.height.toInt();*/
} else if (mime.startsWith('audio/')) {
MoxplatformPlugin.media.scanFile(downloadedPath);
}

View File

@ -4,7 +4,10 @@ import 'dart:typed_data';
import 'dart:ui';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:synchronized/synchronized.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
/// Add a leading zero, if required, to ensure that an integer is rendered
/// as a two "digit" string.
@ -348,3 +351,36 @@ Future<Size?> getImageSizeFromData(Uint8List bytes) async {
return null;
}
}
/// Generate a thumbnail file (JPEG) for the video at [path]. [conversationJid] refers
/// to the JID of the conversation the file comes from.
/// If the thumbnail already exists, then just its path is returned. If not, then
/// it gets generated first.
Future<String> getVideoThumbnailPath(String path, String conversationJid) async {
final tempDir = await getTemporaryDirectory();
final thumbnailFilenameNoExtension = p.withoutExtension(
p.basename(path),
);
final thumbnailFilename = '$thumbnailFilenameNoExtension.jpg';
final thumbnailDirectory = p.join(
tempDir.path,
'thumbnails',
conversationJid,
);
final thumbnailPath = p.join(thumbnailDirectory, thumbnailFilename);
final dir = Directory(thumbnailDirectory);
if (!dir.existsSync()) await dir.create(recursive: true);
final file = File(thumbnailPath);
if (file.existsSync()) return thumbnailPath;
final r = await VideoThumbnail.thumbnailFile(
video: path,
thumbnailPath: thumbnailDirectory,
imageFormat: ImageFormat.JPEG,
quality: 75,
);
assert(r == thumbnailPath, 'The generated video thumbnail has a different path than we expected: $r vs. $thumbnailPath');
return thumbnailPath;
}

View File

@ -63,7 +63,9 @@ class SendFilesPage extends StatelessWidget {
padding: const EdgeInsets.only(right: 4),
child: SharedVideoWidget(
path,
() {
// TODO(PapaTutuWawa): Fix
'sendfiles',
onTap: () {
if (selected) {
// The trash can icon has been tapped
context.read<SendFilesBloc>().add(

View File

@ -5,7 +5,6 @@ import 'package:moxxyv2/ui/widgets/topbar.dart';
import 'package:url_launcher/url_launcher.dart';
// TODO(PapaTutuWawa): Include license text
// TODO(Unknown): Maybe include the version number
class SettingsAboutPage extends StatelessWidget {
const SettingsAboutPage({ super.key });

View File

@ -193,8 +193,9 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
} else if (medium.mime!.startsWith('video/')) {
return SharedVideoWidget(
medium.path,
() => OpenFile.open(medium.path),
child: const PlayButton(),
conversationJid,
onTap: () => OpenFile.open(medium.path),
child: const PlayButton(size: 32),
);
}
// TODO(Unknown): Audio

View File

@ -30,19 +30,23 @@ class VideoChatWidget extends StatelessWidget {
final bool sent;
Widget _buildUploading() {
// TODO(PapaTutuWawa): Fix
return MediaBaseChatWidget(
const Padding(
padding: EdgeInsets.all(32),
child: Icon(
Icons.error_outline,
size: 32,
),
FutureBuilder<String>(
future: getVideoThumbnailPath(message.mediaUrl!, message.conversationJid),
builder: (context, snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Image.file(File(snapshot.data!));
} else {
widget = const Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
);
}
return widget;
},
),
/*VideoThumbnailWidget(
message.mediaUrl!,
Image.memory,
),*/
MessageBubbleBottom(message, sent),
radius,
extra: ProgressWidget(id: message.id),
@ -81,19 +85,23 @@ class VideoChatWidget extends StatelessWidget {
/// The video exists locally
Widget _buildVideo() {
// TODO(PapaTutuWawa): Fix
return MediaBaseChatWidget(
const Padding(
padding: EdgeInsets.all(32),
child: Icon(
Icons.error_outline,
size: 32,
),
FutureBuilder<String>(
future: getVideoThumbnailPath(message.mediaUrl!, message.conversationJid),
builder: (context, snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Image.file(File(snapshot.data!));
} else {
widget = const Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
);
}
return widget;
},
),
/*VideoThumbnailWidget(
message.mediaUrl!,
Image.memory,
),*/
MessageBubbleBottom(message, sent),
radius,
onTap: () {

View File

@ -1,3 +1,4 @@
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';
class PlayButton extends StatelessWidget {
@ -9,17 +10,11 @@ class PlayButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.black45,
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.play_arrow,
size: size,
),
return Align(
child: DecoratedIcon(
Icons.play_arrow,
shadows: const [ BoxShadow(blurRadius: 16) ],
size: size,
),
);
}

View File

@ -1,20 +1,55 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
class SharedVideoWidget extends StatelessWidget {
const SharedVideoWidget(this.path, this.onTap, { this.borderColor, this.child, super.key });
const SharedVideoWidget(
this.path,
this.conversationJid, {
this.onTap,
this.borderColor,
this.child,
this.size = sharedMediaContainerDimension,
this.borderRadius = 10,
super.key,
}
);
final String path;
final String conversationJid;
final Color? borderColor;
final void Function() onTap;
final void Function()? onTap;
final Widget? child;
final double size;
final double borderRadius;
@override
Widget build(BuildContext context) {
return SharedMediaContainer(
const Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
FutureBuilder<String>(
future: getVideoThumbnailPath(path, conversationJid),
builder: (context, snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(File(snapshot.data!)),
),
),
clipBehavior: Clip.hardEdge,
child: child,
);
} else {
widget = const CircularProgressIndicator();
}
return widget;
},
),
size: size,
onTap: onTap,
);
}

View File

@ -10,6 +10,7 @@ import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/widgets/avatar.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
import 'package:moxxyv2/ui/widgets/chat/typing.dart';
class ConversationsListRow extends StatefulWidget {
@ -98,6 +99,29 @@ class ConversationsListRowState extends State<ConversationsListRow> {
return avatar;
}
Widget _buildLastMessagePreview() {
Widget? preview;
if (widget.conversation.lastMessage!.mediaType!.startsWith('image/')) {
preview = SharedImageWidget(
widget.conversation.lastMessage!.mediaUrl!,
borderRadius: 5,
size: 30,
);
} else if (widget.conversation.lastMessage!.mediaType!.startsWith('video/')) {
preview = SharedVideoWidget(
widget.conversation.lastMessage!.mediaUrl!,
widget.conversation.jid,
borderRadius: 5,
size: 30,
);
}
return Padding(
padding: const EdgeInsets.only(right: 5),
child: preview,
);
}
Widget _buildLastMessageBody() {
if (widget.conversation.isTyping) {
@ -233,11 +257,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
...widget.conversation.lastMessage?.isThumbnailable == true && !widget.conversation.isTyping ? [
Padding(
padding: const EdgeInsets.only(right: 5),
child: SharedImageWidget(
widget.conversation.lastMessage!.mediaUrl!,
borderRadius: 5,
size: 30,
),
child: _buildLastMessagePreview(),
),
] : [
const SizedBox(height: 30),

View File

@ -1365,6 +1365,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
video_thumbnail:
dependency: "direct main"
description:
name: video_thumbnail
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.3"
vm_service:
dependency: transitive
description:

View File

@ -94,6 +94,7 @@ dependencies:
url_launcher: 6.1.5
#unifiedpush: 3.0.1
uuid: 3.0.5
video_thumbnail: 0.5.3
dev_dependencies:
build_runner: ^2.1.11