Browse Source

feat(widget): 添加连麦卡片模型和视频通话小窗组件

- 创建 LinkMicCardModel 类用于连麦卡片数据结构
- 实现连麦卡片模型的 JSON 序列化和反序列化
- 添加 VideoCallOverlayWidget 组件用于视频通话小窗显示
- 实现小窗拖拽和边缘吸附功能
- 集成通话时长显示和用户信息展示
- 添加点击小窗返回视频通话页面的功能
master
Jolie 3 months ago
parent
commit
e175fe55de
2 changed files with 230 additions and 0 deletions
  1. 35
      lib/model/rtc/link_mic_card_model.dart
  2. 195
      lib/widget/message/video_call_overlay_widget.dart

35
lib/model/rtc/link_mic_card_model.dart

@ -0,0 +1,35 @@
///
class LinkMicCardModel {
final int type;
final int num;
final int? unitSellingPrice;
LinkMicCardModel({
required this.type,
required this.num,
this.unitSellingPrice,
});
factory LinkMicCardModel.fromJson(Map<String, dynamic> json) {
return LinkMicCardModel(
type: json['type'] as int? ?? 0,
num: json['num'] as int? ?? 0,
unitSellingPrice: json['unitSellingPrice'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'num': num,
'unitSellingPrice': unitSellingPrice,
};
}
@override
String toString() {
return 'LinkMicCardModel(type: $type, num: $num, unitSellingPrice: $unitSellingPrice)';
}
}

195
lib/widget/message/video_call_overlay_widget.dart

@ -0,0 +1,195 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dating_touchme_app/controller/message/call_manager.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/message/video_call_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
///
class VideoCallOverlayWidget extends StatefulWidget {
final VoidCallback? onClose;
final String targetUserId;
final String? targetUserName;
final String? targetAvatarUrl;
const VideoCallOverlayWidget({
super.key,
this.onClose,
required this.targetUserId,
this.targetUserName,
this.targetAvatarUrl,
});
@override
State<VideoCallOverlayWidget> createState() => _VideoCallOverlayWidgetState();
}
class _VideoCallOverlayWidgetState extends State<VideoCallOverlayWidget> {
Offset _position = Offset.zero;
bool _isDragging = false;
final CallManager _callManager = CallManager.instance;
@override
void initState() {
super.initState();
//
WidgetsBinding.instance.addPostFrameCallback((_) {
final size = MediaQuery.of(context).size;
setState(() {
_position = Offset(
size.width - 100.w,
100,
);
});
});
}
///
void _snapToEdge(double screenWidth) {
final centerX = screenWidth / 2;
final targetX = _position.dx < centerX
? 0.0
: screenWidth - 100.w;
setState(() {
_position = Offset(targetX, _position.dy);
_isDragging = false;
});
}
///
String _formatDuration(int seconds) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(seconds ~/ 60);
final secs = twoDigits(seconds % 60);
return '$minutes:$secs';
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return AnimatedPositioned(
duration: _isDragging
? const Duration(milliseconds: 50)
: const Duration(milliseconds: 300),
curve: _isDragging ? Curves.linear : Curves.easeOut,
left: _position.dx,
top: _position.dy,
child: _buildContent(screenSize),
);
}
Widget _buildContent(Size screenSize) {
return GestureDetector(
onPanStart: (details) {
setState(() {
_isDragging = true;
});
},
onPanUpdate: (details) {
setState(() {
_position += details.delta;
_position = Offset(
_position.dx.clamp(0.0, screenSize.width - 100.w),
_position.dy.clamp(0.0, screenSize.height - 100.w),
);
});
},
onPanEnd: (details) {
_snapToEdge(screenSize.width);
},
child: Obx(() {
final callSession = _callManager.currentCall.value;
final isConnected = callSession != null && _callManager.callDurationSeconds.value > 0;
final callDuration = _callManager.callDurationSeconds.value;
return Container(
width: 100.w,
height: 100.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.w),
child: Stack(
children: [
//
Container(
width: 100.w,
height: 100.w,
color: Colors.black,
child: widget.targetAvatarUrl != null && widget.targetAvatarUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: widget.targetAvatarUrl!,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Image.asset(
Assets.imagesUserAvatar,
fit: BoxFit.cover,
),
)
: Image.asset(
Assets.imagesUserAvatar,
fit: BoxFit.cover,
),
),
//
Container(
color: Colors.black.withOpacity(0.4),
),
//
Positioned(
bottom: 8.w,
left: 4.w,
right: 4.w,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
Text(
widget.targetUserName ?? widget.targetUserId,
style: TextStyle(
color: Colors.white,
fontSize: 10.sp,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2.h),
//
Text(
isConnected ? _formatDuration(callDuration) : '通话中',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 8.sp,
),
),
],
),
),
],
),
),
).onTap(() {
//
Get.to(() => VideoCallPage(
targetUserId: widget.targetUserId,
isInitiator: callSession?.isInitiator ?? true,
));
widget.onClose?.call();
});
}),
);
}
}
Loading…
Cancel
Save