yumi-flutter/lib/ui_kit/widgets/room/room_bottom_widget.dart
2026-05-07 10:06:02 +08:00

427 lines
12 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart';
import 'package:yumi/modules/gift/gift_page.dart';
import 'package:yumi/services/gift/room_gift_combo_send_controller.dart';
import 'package:yumi/ui_kit/widgets/room/bottom/room_bottom_chat_entry.dart';
import 'package:yumi/ui_kit/widgets/room/bottom/room_bottom_circle_action.dart';
import 'package:yumi/ui_kit/widgets/room/bottom/room_bottom_gift_button.dart';
import 'package:yumi/ui_kit/widgets/room/bottom/room_gift_combo_floating_button.dart';
import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import '../../../app/routes/sc_fluro_navigator.dart';
import '../../../modules/index/main_route.dart';
import '../../../services/audio/rtm_manager.dart';
import '../../components/sc_debounce_widget.dart';
class RoomBottomWidget extends StatefulWidget {
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
static const double floatingButtonHostHeight = _floatingButtonHostHeight;
final bool showGiftComboButton;
@override
State<RoomBottomWidget> createState() => _RoomBottomWidgetState();
}
class RoomGiftComboFloatingLayer extends StatelessWidget {
const RoomGiftComboFloatingLayer({super.key});
@override
Widget build(BuildContext context) {
return Selector<RtcProvider, bool>(
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
builder: (context, showMic, child) {
return LayoutBuilder(
builder: (context, constraints) {
final inputWidth = _resolveRoomBottomInputWidth(
maxWidth: constraints.maxWidth,
showMic: showMic,
);
final giftCenterX = _resolveRoomGiftCenterX(
maxWidth: constraints.maxWidth,
inputWidth: inputWidth,
showMic: showMic,
);
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: giftCenterX - (_floatingButtonWidth.w / 2),
bottom: _floatingButtonBottomOffset.w,
child: const RoomGiftComboFloatingButton(),
),
],
);
},
);
},
);
}
}
class _RoomBottomWidgetState extends State<RoomBottomWidget> {
int roomMenuStime1 = 0;
@override
void dispose() {
RoomGiftComboSendController().hide();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: _floatingButtonHostHeight.w,
child: Selector<RtcProvider, _RoomBottomSnapshot>(
selector:
(context, provider) => _RoomBottomSnapshot(
showMic: _shouldShowRoomBottomMic(provider),
isMic: provider.isMic,
),
builder: (context, bottomSnapshot, child) {
return LayoutBuilder(
builder: (context, constraints) {
final inputWidth = _resolveRoomBottomInputWidth(
maxWidth: constraints.maxWidth,
showMic: bottomSnapshot.showMic,
);
final giftCenterX = _resolveRoomGiftCenterX(
maxWidth: constraints.maxWidth,
inputWidth: inputWidth,
showMic: bottomSnapshot.showMic,
);
return Stack(
clipBehavior: Clip.none,
children: [
if (widget.showGiftComboButton)
Positioned(
left: giftCenterX - (_floatingButtonWidth.w / 2),
bottom: _floatingButtonBottomOffset.w,
child: const RoomGiftComboFloatingButton(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SizedBox(
height: _bottomBarHeight.w,
child: _buildBottomBar(
inputWidth: inputWidth,
showMic: bottomSnapshot.showMic,
isMic: bottomSnapshot.isMic,
),
),
),
],
);
},
);
},
),
);
}
Widget _buildChatEntry(double inputWidth) {
return RoomBottomChatEntry(
width: inputWidth,
onTap: () {
if (SCRoomUtils.touristCanMsg(context)) {
Navigator.push(context, PopRoute(child: RoomMsgInput()));
}
},
);
}
Widget _buildChatControls(double inputWidth) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildChatEntry(inputWidth),
SizedBox(width: _chatEmojiGap.w),
_buildEmojiAction(),
],
);
}
Widget _buildBottomBar({
required double inputWidth,
required bool showMic,
required bool isMic,
}) {
final giftAction = _buildGiftAction();
final messageAction = _buildMessageAction();
final menuAction = _buildMenuAction();
return Padding(
padding: EdgeInsetsDirectional.only(
start: _horizontalPadding.w,
end: _horizontalPadding.w,
),
child:
showMic
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildChatControls(inputWidth),
giftAction,
_buildMicAction(isMic: isMic),
menuAction,
messageAction,
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildChatControls(inputWidth),
const Spacer(),
giftAction,
SizedBox(width: _compactGap.w),
menuAction,
SizedBox(width: _compactGap.w),
messageAction,
],
),
);
}
Widget _buildGiftAction() {
return SizedBox(
width: _giftActionWidth.w,
height: _giftActionWidth.w,
child: RoomBottomGiftButton(onTap: _showGiftPanel),
);
}
Widget _buildEmojiAction() {
return SizedBox(
width: _emojiActionWidth.w,
height: 48.w,
child: SCDebounceWidget(
onTap: () {
if (SCRoomUtils.touristCanMsg(context)) {
Navigator.push(
context,
PopRoute(child: const RoomMsgInput(initialShowEmoji: true)),
);
}
},
child: Center(
child: Image.asset(
"sc_images/room/sc_icon_room_bottom_emoji.png",
width: 34.w,
height: 34.w,
fit: BoxFit.contain,
gaplessPlayback: true,
),
),
),
);
}
void _showGiftPanel() {
SmartDialog.show(
tag: "showGiftControl",
alignment: Alignment.bottomCenter,
maskColor: Colors.transparent,
animationType: SmartAnimationType.fade,
clickMaskDismiss: true,
builder: (_) {
return GiftPage();
},
);
}
Widget _buildMenuAction() {
return SCDebounceWidget(
onTap: () {
SmartDialog.show(
tag: "showRoomMenuDialog",
alignment: Alignment.bottomCenter,
debounce: true,
animationType: SmartAnimationType.fade,
maskColor: Colors.transparent,
clickMaskDismiss: true,
builder: (_) {
return RoomMenuDialog(roomMenuStime1, (eTime) {
roomMenuStime1 = eTime;
});
},
);
},
child: RoomBottomCircleAction(
child: Image.asset(
"sc_images/room/sc_icon_botton_menu.png",
width: 20.w,
height: 20.w,
fit: BoxFit.contain,
),
),
);
}
Widget _buildMessageAction() {
return SCDebounceWidget(
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.message}?isFromRoom=true",
);
},
child: Selector<RtmProvider, int>(
selector: (c, p) => p.allUnReadCount,
shouldRebuild: (prev, next) => prev != next,
builder: (_, allUnReadCount, __) {
final action = RoomBottomCircleAction(
child: Image.asset(
"sc_images/room/sc_icon_botton_message_custom.png",
width: 22.w,
height: 22.w,
fit: BoxFit.contain,
),
);
if (allUnReadCount <= 0) {
return action;
}
return Badge(
backgroundColor: Colors.red,
label: text(
"${allUnReadCount > 99 ? "99+" : allUnReadCount}",
fontSize: 9.sp,
textColor: Colors.white,
fontWeight: FontWeight.w600,
),
alignment: AlignmentDirectional.topEnd,
child: action,
);
},
),
);
}
Widget _buildMicAction({required bool isMic}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
final provider = context.read<RtcProvider>();
setState(() {
provider.isMic = !provider.isMic;
});
unawaited(provider.setSelfMicMutedFromBottom(provider.isMic));
},
child: RoomBottomCircleAction(
child: Image.asset(
"sc_images/room/${isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png",
width: 30.w,
height: 30.w,
fit: BoxFit.contain,
gaplessPlayback: true,
),
),
);
}
}
const double _bottomBarHeight = 72;
const double _floatingButtonHostHeight = 122;
const double _floatingButtonWidth = RoomGiftComboFloatingButton.hostSize;
const double _floatingButtonBottomOffset = 54;
const double _giftActionWidth = 52;
const double _circleActionWidth = 46;
const double _emojiActionWidth = 38;
const double _chatEmojiGap = 8;
const double _compactGap = 14;
const double _horizontalPadding = 16;
double _resolveRoomBottomInputWidth({
required double maxWidth,
required bool showMic,
}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
final actionWidth =
_emojiActionWidth.w +
_chatEmojiGap.w +
_giftActionWidth.w +
_circleActionWidth.w * (showMic ? 3 : 2) +
(showMic ? 16.w : _compactGap.w * 2);
final availableWidth = contentWidth - actionWidth;
return availableWidth.clamp(82.w, 112.w).toDouble();
}
double _resolveRoomGiftCenterX({
required double maxWidth,
required double inputWidth,
required bool showMic,
}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
if (showMic) {
final occupiedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 3;
final gapCount = 4;
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
0.0,
double.infinity,
);
return _horizontalPadding.w +
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
gap +
(_giftActionWidth.w / 2);
}
final fixedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 2 +
_compactGap.w * 2;
final spacerWidth = (contentWidth - fixedWidth).clamp(0.0, double.infinity);
return _horizontalPadding.w +
inputWidth +
spacerWidth +
(_giftActionWidth.w / 2);
}
bool _shouldShowRoomBottomMic(RtcProvider provider) {
return provider.isOnMai();
}
class _RoomBottomSnapshot {
const _RoomBottomSnapshot({required this.showMic, required this.isMic});
final bool showMic;
final bool isMic;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is _RoomBottomSnapshot &&
other.showMic == showMic &&
other.isMic == isMic;
}
@override
int get hashCode => Object.hash(showMic, isMic);
}