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.
 
 
 
 
 

696 lines
24 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';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/controller/message/chat_controller.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) {
// 检查是否有金币信息(只对接收的消息显示)
final revenueInfo = _getRevenueInfo();
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),
Column(
crossAxisAlignment: widget.isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// 🚀 极致性能优化:无需初始化,直接显示缩略图
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 && revenueInfo != null)
Padding(
padding: EdgeInsets.only(top: 10.h),
child: _buildCoinLabel(revenueInfo),
),
],
),
if (widget.isSentByMe) SizedBox(width: 8.w),
if (widget.isSentByMe) _buildAvatar(),
],
);
},
),
),
],
);
}
// 构建头像
Widget _buildAvatar() {
String? avatarUrl;
if (widget.isSentByMe) {
// 发送的消息:使用当前登录用户的头像
// 优先从消息 attributes 中获取
try {
final attributes = widget.message.attributes;
if (attributes != null) {
avatarUrl = attributes['sender_avatarUrl'] as String? ??
attributes['avatarUrl'] as String?;
}
} catch (e) {
// 忽略错误
}
// 如果消息中没有,使用当前登录用户的头像
if (avatarUrl == null || avatarUrl.isEmpty) {
avatarUrl = GlobalData().userData?.profilePhoto;
}
} else {
// 接收的消息:使用发送者的头像
try {
final attributes = widget.message.attributes;
if (attributes != null) {
avatarUrl = attributes['sender_avatarUrl'] as String? ??
attributes['avatarUrl'] as String?;
}
} catch (e) {
// 忽略错误
}
// 如果消息中没有,尝试从 ChatController 获取对方用户头像
if ((avatarUrl == null || avatarUrl.isEmpty)) {
try {
// 尝试从 Get 获取 ChatController
final chatController = Get.find<ChatController>();
avatarUrl = chatController.userAvatarUrl;
} catch (e) {
// ChatController 可能不存在,忽略错误
}
}
}
// 清理头像URL(移除反引号)
if (avatarUrl != null && avatarUrl.isNotEmpty) {
avatarUrl = avatarUrl.trim().replaceAll('`', '');
}
return ClipOval(
child: avatarUrl != null && avatarUrl.isNotEmpty
? Image.network(
avatarUrl,
width: 40.w,
height: 40.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
Assets.imagesUserAvatar,
width: 40.w,
height: 40.w,
fit: BoxFit.cover,
);
},
)
: Image.asset(
Assets.imagesUserAvatar,
width: 40.w,
height: 40.w,
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: 44.w,
height: 26.h,
decoration: BoxDecoration(
color: Color.fromRGBO(248, 85, 66, 1),
borderRadius: BorderRadius.circular(8.w),
),
child: Center(
child: Text(
'重发',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
),
),
);
} 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();
}
}
// 获取金币信息(从 revenueInfo 或 coin_value 中获取)
String? _getRevenueInfo() {
try {
final attributes = widget.message.attributes;
if (attributes != null) {
// 优先从 revenueInfo 获取
final revenueInfo = attributes['revenueInfo'] as String?;
if (revenueInfo != null && revenueInfo.isNotEmpty) {
return revenueInfo;
}
// 如果没有 revenueInfo,从 coin_value 获取(可能是之前存储的 revenueInfo 值)
final coinValueStr = attributes['coin_value'] as String?;
if (coinValueStr != null && coinValueStr.isNotEmpty) {
return coinValueStr;
}
}
} catch (e) {
// 忽略错误
}
return null;
}
// 构建金币标签
Widget _buildCoinLabel(String revenueInfo) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.h),
decoration: BoxDecoration(
color: Color.fromRGBO(0, 0, 0, 0.05),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
Assets.imagesImCoinIcon,
width: 10.w,
height: 10.w,
),
SizedBox(width: 2.w),
Text(
revenueInfo,
style: TextStyle(
fontSize: 9.sp,
color: Color.fromRGBO(255, 132, 0, 1),
),
),
],
),
);
}
// 🚀 性能优化:构建缓存的缩略图
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;
}
}