diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a364d080..d8e72f8b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -324,6 +324,9 @@ PODS: - Flutter - url_launcher_ios (0.0.1): - Flutter + - video_thumbnail (0.0.1): + - Flutter + - libwebp - wakelock_plus (0.0.1): - Flutter - WebRTC-SDK (137.7151.04) @@ -379,6 +382,7 @@ DEPENDENCIES: - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: @@ -508,6 +512,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + video_thumbnail: + :path: ".symlinks/plugins/video_thumbnail/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" @@ -587,6 +593,7 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e diff --git a/lib/widgets/content/attachment_preview.dart b/lib/widgets/content/attachment_preview.dart index 28e771f3..eef8519a 100644 --- a/lib/widgets/content/attachment_preview.dart +++ b/lib/widgets/content/attachment_preview.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:cross_file/cross_file.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -275,9 +277,184 @@ class AttachmentPreview extends HookConsumerWidget { var ratio = item.isOnCloud ? (item.data.fileMeta?['ratio'] is num ? item.data.fileMeta!['ratio'].toDouble() - : 1.0) - : 1.0; - if (ratio == 0) ratio = 1.0; + : null) + : null; + + final innerContentWidget = Stack( + fit: StackFit.expand, + children: [ + HookBuilder( + key: ValueKey(item.hashCode), + builder: (context) { + final fallbackIcon = switch (item.type) { + UniversalFileType.video => Symbols.video_file, + UniversalFileType.audio => Symbols.audio_file, + UniversalFileType.image => Symbols.image, + _ => Symbols.insert_drive_file, + }; + + final mimeType = FileUploader.getMimeType(item); + + if (item.isOnCloud) { + return CloudFileWidget(item: item.data); + } else if (item.data is XFile) { + final file = item.data as XFile; + if (file.path.isEmpty) { + return FutureBuilder( + future: file.readAsBytes(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Image.memory(snapshot.data!); + } + return const Center(child: CircularProgressIndicator()); + }, + ); + } + + switch (item.type) { + case UniversalFileType.image: + return kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path)); + case UniversalFileType.video: + if (!kIsWeb) { + final thumbnailFuture = useMemoized( + () => VideoThumbnail.thumbnailData( + video: file.path, + imageFormat: ImageFormat.JPEG, + maxWidth: 320, + quality: 50, + ), + [file.path], + ); + return FutureBuilder( + future: thumbnailFuture, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data != null) { + return Stack( + children: [ + Image.memory(snapshot.data!), + Positioned.fill( + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: const Icon( + Symbols.play_arrow, + color: Colors.white, + size: 32, + ), + ), + ), + ), + ], + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ); + } + break; + default: + break; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(fallbackIcon), + const Gap(6), + Text( + _getDisplayName(), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text(mimeType, style: TextStyle(fontSize: 10)), + const Gap(1), + FutureBuilder( + future: file.length(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final size = snapshot.data as int; + return Text(formatFileSize(size)).fontSize(11); + } + return const SizedBox.shrink(); + }, + ), + ], + ).padding(vertical: 32); + } else if (item is List || item is Uint8List) { + switch (item.type) { + case UniversalFileType.image: + return Image.memory(item.data); + default: + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(fallbackIcon), + const Gap(6), + Text(mimeType, style: TextStyle(fontSize: 10)), + const Gap(1), + Text(formatFileSize(item.data.length)).fontSize(11), + ], + ); + } + } + return Placeholder(); + }, + ), + if (isUploading && progress != null && (progress ?? 0) > 0) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.3), + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${(progress! * 100).toStringAsFixed(2)}%', + style: TextStyle(color: Colors.white), + ), + Gap(6), + Center( + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: progress), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + builder: (context, value, child) => + LinearProgressIndicator(value: value), + ), + ), + ], + ), + ), + ), + if (isUploading && (progress == null || progress == 0)) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.3), + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'processing'.tr(), + style: TextStyle(color: Colors.white), + ), + Gap(6), + Center(child: LinearProgressIndicator(value: null)), + ], + ), + ), + ), + ], + ); final contentWidget = ClipRRect( borderRadius: BorderRadius.circular(8), @@ -285,149 +462,13 @@ class AttachmentPreview extends HookConsumerWidget { color: Theme.of(context).colorScheme.surfaceContainer, child: Stack( children: [ - AspectRatio( - aspectRatio: ratio, - child: Stack( - fit: StackFit.expand, - children: [ - Builder( - key: ValueKey(item.hashCode), - builder: (context) { - final fallbackIcon = switch (item.type) { - UniversalFileType.video => Symbols.video_file, - UniversalFileType.audio => Symbols.audio_file, - UniversalFileType.image => Symbols.image, - _ => Symbols.insert_drive_file, - }; - - final mimeType = FileUploader.getMimeType(item); - - if (item.isOnCloud) { - return CloudFileWidget(item: item.data); - } else if (item.data is XFile) { - final file = item.data as XFile; - if (file.path.isEmpty) { - return FutureBuilder( - future: file.readAsBytes(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Image.memory(snapshot.data!); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - } - - switch (item.type) { - case UniversalFileType.image: - return kIsWeb - ? Image.network(file.path) - : Image.file(File(file.path)); - default: - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(fallbackIcon), - const Gap(6), - Text( - _getDisplayName(), - textAlign: TextAlign.center, - ), - Text(mimeType, style: TextStyle(fontSize: 10)), - const Gap(1), - FutureBuilder( - future: file.length(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final size = snapshot.data as int; - return Text( - formatFileSize(size), - ).fontSize(11); - } - return const SizedBox.shrink(); - }, - ), - ], - ); - } - } else if (item is List || item is Uint8List) { - switch (item.type) { - case UniversalFileType.image: - return Image.memory(item.data); - default: - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(fallbackIcon), - const Gap(6), - Text(mimeType, style: TextStyle(fontSize: 10)), - const Gap(1), - Text( - formatFileSize(item.data.length), - ).fontSize(11), - ], - ); - } - } - return Placeholder(); - }, - ), - if (isUploading && progress != null && (progress ?? 0) > 0) - Positioned.fill( - child: Container( - color: Colors.black.withOpacity(0.3), - padding: EdgeInsets.symmetric( - horizontal: 40, - vertical: 16, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '${(progress! * 100).toStringAsFixed(2)}%', - style: TextStyle(color: Colors.white), - ), - Gap(6), - Center( - child: TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: progress), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - builder: (context, value, child) => LinearProgressIndicator(value: value), - ), - ), - ], - ), - ), - ), - if (isUploading && (progress == null || progress == 0)) - Positioned.fill( - child: Container( - color: Colors.black.withOpacity(0.3), - padding: EdgeInsets.symmetric( - horizontal: 40, - vertical: 16, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'processing'.tr(), - style: TextStyle(color: Colors.white), - ), - Gap(6), - Center(child: LinearProgressIndicator(value: null)), - ], - ), - ), - ), - ], - ), - ).center(), + if (ratio != null) + AspectRatio( + aspectRatio: ratio, + child: innerContentWidget, + ).center() + else + IntrinsicHeight(child: innerContentWidget), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -605,4 +646,4 @@ class AttachmentPreview extends HookConsumerWidget { child: contentWidget, ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 9a54c4bb..993be289 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -3195,6 +3195,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + video_thumbnail: + dependency: "direct main" + description: + name: video_thumbnail + sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" + url: "https://pub.dev" + source: hosted + version: "0.5.6" visibility_detector: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2ab6dc68..c0729689 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -175,6 +175,7 @@ dependencies: in_app_review: ^2.0.11 snow_fall_animation: ^0.0.1+3 flutter_app_intents: ^0.7.0 + video_thumbnail: ^0.5.6 dev_dependencies: flutter_test: @@ -274,4 +275,4 @@ msix_config: logo_path: .\assets\icons\icon.png protocol_activation: solian, https app_uri_handler_hosts: solian.app - capabilities: internetClientServer, location, microphone, webcam + capabilities: internetClientServer, location, microphone, webcam \ No newline at end of file