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,9 +277,184 @@ 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 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<Uint8List>(
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<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:
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<int> || 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<double>(
tween: Tween<double>(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( final contentWidget = ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -285,149 +462,13 @@ class AttachmentPreview extends HookConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack( child: Stack(
children: [ children: [
AspectRatio( if (ratio != null)
aspectRatio: ratio, AspectRatio(
child: Stack( aspectRatio: ratio,
fit: StackFit.expand, child: innerContentWidget,
children: [ ).center()
Builder( else
key: ValueKey(item.hashCode), IntrinsicHeight(child: innerContentWidget),
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<Uint8List>(
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<int> || 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<double>(
tween: Tween<double>(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(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -605,4 +646,4 @@ class AttachmentPreview extends HookConsumerWidget {
child: contentWidget, child: contentWidget,
); );
} }
} }

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:
@@ -274,4 +275,4 @@ msix_config:
logo_path: .\assets\icons\icon.png logo_path: .\assets\icons\icon.png
protocol_activation: solian, https protocol_activation: solian, https
app_uri_handler_hosts: solian.app app_uri_handler_hosts: solian.app
capabilities: internetClientServer, location, microphone, webcam capabilities: internetClientServer, location, microphone, webcam