You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

565 lines
20 KiB

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'package:get/get.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:video_player/video_player.dart';
import 'package:dating_touchme_app/pages/message/video_player_page.dart';
import 'package:dating_touchme_app/generated/assets.dart';
class VideoItem extends StatefulWidget {
final EMVideoMessageBody videoBody;
final bool isSentByMe;
final bool showTime;
final String formattedTime;
final EMMessage message; // 添加消息对象以获取状态
final VoidCallback? onResend; // 添加重发回调
const VideoItem({
required this.videoBody,
required this.isSentByMe,
required this.showTime,
required this.formattedTime,
required this.message,
this.onResend,
super.key,
});
@override
State<VideoItem> createState() => _VideoItemState();
}
// 消息发送状态枚举
enum MessageSendStatus {
sending,
success,
failed,
}
class _VideoItemState extends State<VideoItem> {
String? _thumbnailPath;
bool _isLoadingVideo = false;
@override
void initState() {
super.initState();
// 🚀 极致性能优化:只准备缩略图路径,完全不初始化视频控制器
_prepareThumbnail();
}
/// 准备缩略图路径(轻量级操作)
void _prepareThumbnail() {
final thumbLocal = widget.videoBody.thumbnailLocalPath;
final thumbRemote = widget.videoBody.thumbnailRemotePath;
print('🖼️ [VideoItem] 缩略图调试信息:');
print('本地缩略图路径: $thumbLocal');
print('远程缩略图路径: $thumbRemote');
// 优先使用本地缩略图
if (thumbLocal != null && thumbLocal.isNotEmpty) {
final file = File(thumbLocal);
if (file.existsSync()) {
_thumbnailPath = thumbLocal;
print('✅ 使用本地缩略图: $thumbLocal');
return;
} else {
print('⚠️ 本地缩略图文件不存在: $thumbLocal');
}
}
// 使用远程缩略图
if (thumbRemote != null && thumbRemote.isNotEmpty) {
_thumbnailPath = thumbRemote;
print('✅ 使用远程缩略图: $thumbRemote');
return;
}
// 🎯 备选方案:如果没有缩略图,尝试使用视频第一帧
final videoLocal = widget.videoBody.localPath;
final videoRemote = widget.videoBody.remotePath;
print('⚠️ 没有缩略图,尝试使用视频路径作为预览');
print('本地视频路径: $videoLocal');
print('远程视频路径: $videoRemote');
// 如果有本地视频,使用视频文件生成预览
if (videoLocal.isNotEmpty && File(videoLocal).existsSync()) {
print('💡 [VideoItem] 将使用视频第一帧作为预览');
_generateThumbnailFromVideo(videoLocal);
} else {
print('❌ 没有可用的缩略图和视频,将显示占位符图标');
}
}
/// 🎯 从视频生成缩略图(备选方案)
void _generateThumbnailFromVideo(String videoPath) async {
try {
print('🎬 [VideoItem] 开始从视频生成缩略图...');
// 使用 video_player 获取第一帧
final controller = VideoPlayerController.file(File(videoPath));
await controller.initialize();
// 设置缩略图路径为视频路径(让 UI 知道要使用视频预览)
if (mounted) {
setState(() {
_thumbnailPath = 'video:$videoPath'; // 特殊标记
});
print('✅ [VideoItem] 使用视频第一帧作为预览');
}
// 释放控制器
controller.dispose();
} catch (e) {
print('❌ [VideoItem] 生成视频预览失败: $e');
}
}
@override
void dispose() {
// 无需释放资源,因为没有初始化任何控制器
super.dispose();
}
// 构建时间标签
Widget _buildTimeLabel() {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
child: Text(
widget.formattedTime,
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
),
);
}
// 格式化时长
String _formatDuration(int seconds) {
final minutes = seconds ~/ 60;
final secs = seconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
}
// 播放视频
void _playVideo() async {
// 如果正在加载,不处理点击
if (_isLoadingVideo) {
return;
}
// 获取视频路径
final localPath = widget.videoBody.localPath;
final remotePath = widget.videoBody.remotePath;
String? videoPath;
bool isNetwork = false;
// 优先使用本地路径
if (localPath.isNotEmpty && File(localPath).existsSync()) {
videoPath = localPath;
isNetwork = false;
} else if (remotePath != null && remotePath.isNotEmpty) {
videoPath = remotePath;
isNetwork = true;
}
if (videoPath != null) {
// 显示加载状态(如果是网络视频)
if (isNetwork) {
setState(() {
_isLoadingVideo = true;
});
// 模拟加载延迟(可选)
await Future.delayed(Duration(milliseconds: 300));
}
// 使用 Chewie 播放器页面
await Get.to(
() => VideoPlayerPage(
videoPath: videoPath!,
isNetwork: isNetwork,
),
transition: Transition.fade,
duration: const Duration(milliseconds: 200),
);
// 隐藏加载状态
if (mounted && isNetwork) {
setState(() {
_isLoadingVideo = false;
});
}
} else {
SmartDialog.showToast('⚠️ 视频路径不可用,请稍后重试');
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (widget.showTime) _buildTimeLabel(),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Builder(
builder: (context) {
// 计算视频尺寸
double maxWidth = 180.w;
double width = maxWidth;
double height = width * (304 / 289); // 按289:304比例计算高度
final videoHeight = height + 10.h; // 加上 margin top (10.h)
return Row(
mainAxisAlignment: widget.isSentByMe
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!widget.isSentByMe) _buildAvatar(),
if (!widget.isSentByMe) SizedBox(width: 8.w),
// 发送消息时,状态在左侧,与视频垂直居中对齐
if (widget.isSentByMe)
SizedBox(
height: videoHeight,
child: Center(
child: _buildMessageStatus(),
),
),
if (widget.isSentByMe) SizedBox(width: 10.w),
// 🚀 极致性能优化:无需初始化,直接显示缩略图
Stack(
children: [
GestureDetector(
onTap: _playVideo,
child: Container(
margin: EdgeInsets.only(top: 10.h),
width: 180.w,
height: 180.w * (304 / 289), // 按289:304比例计算高度
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(18.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18.w),
child: Stack(
fit: StackFit.expand,
children: [
// 🚀 背景层:默认占位符
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey[300]!,
Colors.grey[400]!,
],
),
),
child: Icon(
Icons.videocam,
size: 48.w,
color: Colors.grey[600],
),
),
// 🚀 缩略图层:始终显示缩略图(如果有)
if (_thumbnailPath != null && _thumbnailPath!.isNotEmpty)
_buildThumbnail(),
// 播放按钮和时长覆盖层
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.black.withOpacity(0.5),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 播放按钮(增强动画效果)
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
Icons.play_arrow_rounded,
size: 48.w,
color: Colors.white,
),
),
],
),
),
// 右下角显示时长标签
Positioned(
right: 8.w,
bottom: 8.h,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(4.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.videocam_rounded,
size: 12.w,
color: Colors.white,
),
SizedBox(width: 4.w),
Text(
_formatDuration(widget.videoBody.duration ?? 0),
style: TextStyle(
fontSize: 11.sp,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
),
),
// 🚀 加载指示器(播放时显示)
if (_isLoadingVideo)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(18.w),
),
child: Center(
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
),
),
],
),
if (widget.isSentByMe) SizedBox(width: 8.w),
if (widget.isSentByMe) _buildAvatar(),
],
);
},
),
),
],
);
}
// 构建头像
Widget _buildAvatar() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
);
}
// 构建消息状态(发送中、已发送、失败重发)
Widget _buildMessageStatus() {
// 只对发送的消息显示状态
if (!widget.isSentByMe) {
return SizedBox.shrink();
}
// 检查消息状态
final status = widget.message.status;
if (status == MessageStatus.FAIL) {
// 发送失败,显示重发按钮
return GestureDetector(
onTap: widget.onResend,
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.w,
color: Colors.red,
),
),
);
} else if (status == MessageStatus.PROGRESS) {
// 发送中,显示加载动画
return Container(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey,
),
),
);
} else {
// 发送成功,不显示任何状态
return SizedBox.shrink();
}
}
// 🚀 性能优化:构建缓存的缩略图
Widget _buildThumbnail() {
if (_thumbnailPath == null || _thumbnailPath!.isEmpty) {
print('⚠️ [VideoItem] 缩略图路径为空,不渲染');
return const SizedBox.shrink();
}
print('🎨 [VideoItem] 开始渲染缩略图: $_thumbnailPath');
// 🎯 检查是否是视频预览模式
if (_thumbnailPath!.startsWith('video:')) {
print('🎬 [VideoItem] 使用视频第一帧预览模式');
final videoPath = _thumbnailPath!.substring(6); // 移除 "video:" 前缀
return _buildVideoPreview(videoPath);
}
// 判断是本地路径还是远程路径
if (_thumbnailPath!.startsWith('http')) {
print('🌐 [VideoItem] 渲染网络缩略图');
// 🚀 使用 CachedNetworkImage 缓存网络图片
return CachedNetworkImage(
imageUrl: _thumbnailPath!,
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
placeholder: (context, url) {
print('⏳ [VideoItem] 网络缩略图加载中...');
return Container(
color: Colors.grey[300],
child: Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.grey[600],
),
),
),
);
},
errorWidget: (context, url, error) {
print('❌ [VideoItem] 网络缩略图加载失败: $error');
return const SizedBox.shrink();
},
// 🚀 内存缓存配置
memCacheWidth: 400, // 限制缓存图片宽度
memCacheHeight: 300, // 限制缓存图片高度
);
} else {
print('📁 [VideoItem] 渲染本地缩略图');
// 本地文件
final file = File(_thumbnailPath!);
if (file.existsSync()) {
print('✅ [VideoItem] 本地缩略图文件存在,开始渲染');
return Image.file(
file,
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
// 🚀 设置缓存宽高,减少内存占用
cacheWidth: 400,
cacheHeight: 300,
errorBuilder: (context, error, stackTrace) {
print('❌ [VideoItem] 本地缩略图渲染失败: $error');
return const SizedBox.shrink();
},
);
} else {
print('❌ [VideoItem] 本地缩略图文件不存在: $_thumbnailPath');
return const SizedBox.shrink();
}
}
}
// 🎯 构建视频第一帧预览(备选方案)
Widget _buildVideoPreview(String videoPath) {
return FutureBuilder<VideoPlayerController>(
future: _initVideoPreview(videoPath),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&
snapshot.data!.value.isInitialized) {
print('✅ [VideoItem] 视频预览加载成功');
// 🎯 使用 FittedBox 保持视频比例,避免变形
return FittedBox(
fit: BoxFit.contain,
child: SizedBox(
width: snapshot.data!.value.size.width,
height: snapshot.data!.value.size.height,
child: VideoPlayer(snapshot.data!),
),
);
} else {
print('⏳ [VideoItem] 视频预览加载中...');
return Container(
color: Colors.grey[300],
child: Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.grey[600],
),
),
),
);
}
},
);
}
// 初始化视频预览
Future<VideoPlayerController> _initVideoPreview(String videoPath) async {
final controller = VideoPlayerController.file(File(videoPath));
await controller.initialize();
await controller.seekTo(Duration.zero); // 定位到第一帧
return controller;
}
}