diff --git a/lib/model/rtc/link_mic_card_model.dart b/lib/model/rtc/link_mic_card_model.dart new file mode 100644 index 0000000..83b55b8 --- /dev/null +++ b/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 json) { + + return LinkMicCardModel( + type: json['type'] as int? ?? 0, + num: json['num'] as int? ?? 0, + unitSellingPrice: json['unitSellingPrice'] as int? ?? 0, + ); + } + + Map toJson() { + return { + 'type': type, + 'num': num, + 'unitSellingPrice': unitSellingPrice, + }; + } + + @override + String toString() { + return 'LinkMicCardModel(type: $type, num: $num, unitSellingPrice: $unitSellingPrice)'; + } +} + diff --git a/lib/widget/message/video_call_overlay_widget.dart b/lib/widget/message/video_call_overlay_widget.dart new file mode 100644 index 0000000..db82ade --- /dev/null +++ b/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 createState() => _VideoCallOverlayWidgetState(); +} + +class _VideoCallOverlayWidgetState extends State { + 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(); + }); + }), + ); + } +} +