Attachment video preview

This commit is contained in:
2026-01-11 16:56:41 +08:00
parent a984cba2fa
commit d8c33b576f
4 changed files with 205 additions and 148 deletions

View File

@@ -324,6 +324,9 @@ PODS:
- Flutter - Flutter
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_thumbnail (0.0.1):
- Flutter
- libwebp
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (137.7151.04) - WebRTC-SDK (137.7151.04)
@@ -379,6 +382,7 @@ DEPENDENCIES:
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`) - syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/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`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS: SPEC REPOS:
@@ -508,6 +512,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios" :path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
wakelock_plus: wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
@@ -587,6 +593,7 @@ SPEC CHECKSUMS:
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14 syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e

View File

@@ -2,6 +2,8 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cross_file/cross_file.dart'; 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:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -275,22 +277,13 @@ class AttachmentPreview extends HookConsumerWidget {
var ratio = item.isOnCloud var ratio = item.isOnCloud
? (item.data.fileMeta?['ratio'] is num ? (item.data.fileMeta?['ratio'] is num
? item.data.fileMeta!['ratio'].toDouble() ? item.data.fileMeta!['ratio'].toDouble()
: 1.0) : null)
: 1.0; : null;
if (ratio == 0) ratio = 1.0;
final contentWidget = ClipRRect( final innerContentWidget = Stack(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
children: [
AspectRatio(
aspectRatio: ratio,
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Builder( HookBuilder(
key: ValueKey(item.hashCode), key: ValueKey(item.hashCode),
builder: (context) { builder: (context) {
final fallbackIcon = switch (item.type) { final fallbackIcon = switch (item.type) {
@@ -313,9 +306,7 @@ class AttachmentPreview extends HookConsumerWidget {
if (snapshot.hasData) { if (snapshot.hasData) {
return Image.memory(snapshot.data!); return Image.memory(snapshot.data!);
} }
return const Center( return const Center(child: CircularProgressIndicator());
child: CircularProgressIndicator(),
);
}, },
); );
} }
@@ -325,7 +316,52 @@ class AttachmentPreview extends HookConsumerWidget {
return kIsWeb return kIsWeb
? Image.network(file.path) ? Image.network(file.path)
: Image.file(File(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<Uint8List?>(
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: default:
break;
}
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -334,6 +370,8 @@ class AttachmentPreview extends HookConsumerWidget {
Text( Text(
_getDisplayName(), _getDisplayName(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
Text(mimeType, style: TextStyle(fontSize: 10)), Text(mimeType, style: TextStyle(fontSize: 10)),
const Gap(1), const Gap(1),
@@ -342,16 +380,13 @@ class AttachmentPreview extends HookConsumerWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final size = snapshot.data as int; final size = snapshot.data as int;
return Text( return Text(formatFileSize(size)).fontSize(11);
formatFileSize(size),
).fontSize(11);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
), ),
], ],
); ).padding(vertical: 32);
}
} else if (item is List<int> || item is Uint8List) { } else if (item is List<int> || item is Uint8List) {
switch (item.type) { switch (item.type) {
case UniversalFileType.image: case UniversalFileType.image:
@@ -364,9 +399,7 @@ class AttachmentPreview extends HookConsumerWidget {
const Gap(6), const Gap(6),
Text(mimeType, style: TextStyle(fontSize: 10)), Text(mimeType, style: TextStyle(fontSize: 10)),
const Gap(1), const Gap(1),
Text( Text(formatFileSize(item.data.length)).fontSize(11),
formatFileSize(item.data.length),
).fontSize(11),
], ],
); );
} }
@@ -378,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
horizontal: 40,
vertical: 16,
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -396,7 +426,8 @@ class AttachmentPreview extends HookConsumerWidget {
tween: Tween<double>(begin: 0.0, end: progress), tween: Tween<double>(begin: 0.0, end: progress),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
builder: (context, value, child) => LinearProgressIndicator(value: value), builder: (context, value, child) =>
LinearProgressIndicator(value: value),
), ),
), ),
], ],
@@ -407,10 +438,7 @@ class AttachmentPreview extends HookConsumerWidget {
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
horizontal: 40,
vertical: 16,
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -426,8 +454,21 @@ class AttachmentPreview extends HookConsumerWidget {
), ),
), ),
], ],
), );
).center(),
final contentWidget = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
children: [
if (ratio != null)
AspectRatio(
aspectRatio: ratio,
child: innerContentWidget,
).center()
else
IntrinsicHeight(child: innerContentWidget),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@@ -3195,6 +3195,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: visibility_detector:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -175,6 +175,7 @@ dependencies:
in_app_review: ^2.0.11 in_app_review: ^2.0.11
snow_fall_animation: ^0.0.1+3 snow_fall_animation: ^0.0.1+3
flutter_app_intents: ^0.7.0 flutter_app_intents: ^0.7.0
video_thumbnail: ^0.5.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: