feat: super chat

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-09-01 21:15:29 +08:00
parent e6af0ef15b
commit 80fa0240e9
20 changed files with 896 additions and 261 deletions

View File

@@ -1,19 +1,24 @@
import 'dart:io';
import 'dart:ui';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
import 'package:PiliPlus/pages/live_room/controller.dart';
import 'package:PiliPlus/pages/live_room/send_danmaku/view.dart';
import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart';
import 'package:PiliPlus/pages/live_room/superchat/superchat_panel.dart';
import 'package:PiliPlus/pages/live_room/widgets/bottom_control.dart';
import 'package:PiliPlus/pages/live_room/widgets/chat.dart';
import 'package:PiliPlus/pages/live_room/widgets/chat_panel.dart';
import 'package:PiliPlus/pages/live_room/widgets/header_control.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/plugin/pl_player/models/play_status.dart';
import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart';
import 'package:PiliPlus/plugin/pl_player/view.dart';
import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart';
import 'package:PiliPlus/services/service_locator.dart';
import 'package:PiliPlus/utils/duration_util.dart';
import 'package:PiliPlus/utils/extension.dart';
@@ -24,6 +29,7 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -44,8 +50,10 @@ class _LiveRoomPageState extends State<LiveRoomPage>
late final PlPlayerController plPlayerController;
bool get isFullScreen => plPlayerController.isFullScreen.value;
final GlobalKey chatKey = GlobalKey();
final GlobalKey playerKey = GlobalKey();
late final GlobalKey pageKey = GlobalKey();
late final GlobalKey chatKey = GlobalKey();
late final GlobalKey scKey = GlobalKey();
late final GlobalKey playerKey = GlobalKey();
@override
void initState() {
@@ -193,7 +201,128 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Alignment? alignment,
bool needDm = true,
}) {
if (!isFullScreen) {
_liveRoomController.fsSC.value = null;
}
_liveRoomController.isFullScreen = isFullScreen;
Widget player = Obx(() {
if (_liveRoomController.isLoaded.value) {
final roomInfoH5 = _liveRoomController.roomInfoH5.value;
return PLVideoPlayer(
key: playerKey,
maxWidth: width,
maxHeight: height,
fill: fill,
alignment: alignment,
plPlayerController: plPlayerController,
headerControl: LiveHeaderControl(
title: roomInfoH5?.roomInfo?.title,
upName: roomInfoH5?.anchorInfo?.baseInfo?.uname,
plPlayerController: plPlayerController,
onSendDanmaku: onSendDanmaku,
onPlayAudio: _liveRoomController.queryLiveUrl,
),
bottomControl: BottomControl(
plPlayerController: plPlayerController,
liveRoomCtr: _liveRoomController,
onRefresh: _liveRoomController.queryLiveUrl,
),
danmuWidget: !needDm
? null
: LiveDanmaku(
liveRoomController: _liveRoomController,
plPlayerController: plPlayerController,
isFullScreen: isFullScreen,
isPipMode: isPipMode,
),
);
}
return const SizedBox.shrink();
});
if (isFullScreen && _liveRoomController.showSuperChat) {
player = Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: player),
if (kDebugMode) ...[
Positioned(
top: 50,
right: 0,
child: TextButton(
onPressed: () {
_liveRoomController.fsSC.value = SuperChatItem.fromJson({
"id": Utils.generateRandomString(8),
"price": 66,
"end_time":
DateTime.now().millisecondsSinceEpoch ~/ 1000 + 5,
"message": Utils.generateRandomString(55),
"user_info": {
"face": "",
"uname": Utils.generateRandomString(8),
},
});
},
child: const Text('add superchat'),
),
),
Positioned(
right: 0,
top: 90,
child: TextButton(
onPressed: () {
_liveRoomController.fsSC.value = null;
},
child: const Text('remove superchat'),
),
),
],
Positioned(
left: padding.left + 25,
bottom: 25,
child: Obx(() {
final item = _liveRoomController.fsSC.value;
if (item == null) {
return const SizedBox.shrink();
}
try {
return SizedBox(
key: Key(item.id.toString()),
width: 255,
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.only(right: 6, top: 6),
child: SuperChatCard(
item: item,
onRemove: () => _liveRoomController.fsSC.value = null,
),
),
Positioned(
right: 0,
top: 0,
child: iconButton(
size: 24,
iconSize: 14,
context: context,
bgColor: const Color(0xEEFFFFFF),
iconColor: Colors.black54,
icon: Icons.clear,
onPressed: () =>
_liveRoomController.fsSC.value = null,
),
),
],
),
);
} catch (_) {
return const SizedBox.shrink();
}
}),
),
],
);
}
return PopScope(
canPop: !isFullScreen,
onPopInvokedWithResult: (bool didPop, Object? result) {
@@ -201,40 +330,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
plPlayerController.triggerFullScreen(status: false);
}
},
child: Obx(() {
if (_liveRoomController.isLoaded.value) {
final roomInfoH5 = _liveRoomController.roomInfoH5.value;
return PLVideoPlayer(
key: playerKey,
maxWidth: width,
maxHeight: height,
fill: fill,
alignment: alignment,
plPlayerController: plPlayerController,
headerControl: LiveHeaderControl(
title: roomInfoH5?.roomInfo?.title,
upName: roomInfoH5?.anchorInfo?.baseInfo?.uname,
plPlayerController: plPlayerController,
onSendDanmaku: onSendDanmaku,
onPlayAudio: _liveRoomController.queryLiveUrl,
),
bottomControl: BottomControl(
plPlayerController: plPlayerController,
liveRoomCtr: _liveRoomController,
onRefresh: _liveRoomController.queryLiveUrl,
),
danmuWidget: !needDm
? null
: LiveDanmaku(
liveRoomController: _liveRoomController,
plPlayerController: plPlayerController,
isFullScreen: isFullScreen,
isPipMode: isPipMode,
),
);
}
return const SizedBox.shrink();
}),
child: player,
);
}
@@ -344,7 +440,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Widget get _buildPP {
final isFullScreen = this.isFullScreen;
final bottomHeight = 80.0 + padding.bottom;
final bottomHeight = 70 + padding.bottom;
final topPadding = padding.top + kToolbarHeight;
final videoHeight = maxHeight - bottomHeight - topPadding;
return Stack(
@@ -562,7 +658,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Widget get _buildBodyH {
final videoWidth =
clampDouble(maxHeight / maxWidth * 1.08, 0.58, 0.75) * maxWidth;
clampDouble(maxHeight / maxWidth * 1.08, 0.56, 0.7) * maxWidth;
final videoHeight = maxHeight - padding.top;
return Obx(
() {
@@ -619,148 +715,215 @@ class _LiveRoomPageState extends State<LiveRoomPage>
],
);
Widget _buildChatWidget([bool isPP = false]) => Padding(
padding: EdgeInsets.only(bottom: 16, top: !isPortrait ? 0 : 16),
child: LiveRoomChat(
Widget _buildChatWidget([bool isPP = false]) {
Widget chat() => LiveRoomChatPanel(
key: chatKey,
isPP: isPP,
roomId: _liveRoomController.roomId,
liveRoomController: _liveRoomController,
),
);
);
return Padding(
padding: EdgeInsets.only(bottom: 12, top: !isPortrait ? 0 : 12),
child: _liveRoomController.showSuperChat
? PageView(
key: pageKey,
controller: _liveRoomController.pageController,
physics: const CustomTabBarViewClampingScrollPhysics(),
onPageChanged: (value) =>
_liveRoomController.pageIndex.value = value,
children: [
KeepAliveWrapper(builder: (context) => chat()),
KeepAliveWrapper(
builder: (context) => SuperChatPanel(
key: scKey,
controller: _liveRoomController,
),
),
],
)
: chat(),
);
}
Widget get _buildInputWidget => Container(
padding: EdgeInsets.only(
top: 5,
left: 10,
right: 10,
bottom: 15 + padding.bottom,
),
height: 80 + padding.bottom,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
Widget get _buildInputWidget {
final child = Container(
padding: EdgeInsets.only(
top: 5,
left: 10,
right: 10,
bottom: padding.bottom,
),
border: Border(
top: BorderSide(color: Color(0x1AFFFFFF)),
height: 70 + padding.bottom,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
border: Border(top: BorderSide(color: Color(0x1AFFFFFF))),
color: Color(0x1AFFFFFF),
),
color: Color(0x1AFFFFFF),
),
child: GestureDetector(
onTap: onSendDanmaku,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(top: 5, bottom: 10),
child: Row(
spacing: 6,
children: [
Obx(
() {
final enableShowDanmaku =
plPlayerController.enableShowDanmaku.value;
return ComBtn(
onTap: () {
final newVal = !enableShowDanmaku;
plPlayerController.enableShowDanmaku.value = newVal;
if (!plPlayerController.tempPlayerConf) {
GStorage.setting.put(
SettingBoxKey.enableShowDanmaku,
newVal,
);
}
},
icon: enableShowDanmaku
? const Icon(
size: 22,
Icons.subtitles_outlined,
color: Color(0xFFEEEEEE),
)
: const Icon(
size: 22,
Icons.subtitles_off_outlined,
color: Color(0xFFEEEEEE),
child: GestureDetector(
onTap: onSendDanmaku,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(top: 5, bottom: 10),
child: Align(
alignment: Alignment.topCenter,
child: Row(
spacing: 6,
children: [
Obx(
() {
final enableShowDanmaku =
plPlayerController.enableShowDanmaku.value;
return SizedBox(
width: 34,
height: 34,
child: IconButton(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
),
);
},
),
const Expanded(
child: Text(
'发送弹幕',
style: TextStyle(color: Color(0xFFEEEEEE)),
),
),
Builder(
builder: (context) {
final theme = Theme.of(context).colorScheme;
return Material(
type: MaterialType.transparency,
child: Stack(
clipBehavior: Clip.none,
children: [
InkWell(
overlayColor: overlayColor(theme),
customBorder: const CircleBorder(),
onTapDown: _liveRoomController.onLikeTapDown,
onTapUp: _liveRoomController.onLikeTapUp,
onTapCancel: _liveRoomController.onLikeTapUp,
child: const SizedBox.square(
dimension: 34,
child: Icon(
size: 22,
color: Color(0xFFEEEEEE),
Icons.thumb_up_off_alt,
),
),
),
Positioned(
right: -12,
top: -12,
child: Obx(() {
final likeClickTime =
_liveRoomController.likeClickTime.value;
if (likeClickTime == 0) {
return const SizedBox.shrink();
onPressed: () {
final newVal = !enableShowDanmaku;
plPlayerController.enableShowDanmaku.value = newVal;
if (!plPlayerController.tempPlayerConf) {
GStorage.setting.put(
SettingBoxKey.enableShowDanmaku,
newVal,
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 160),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: Text(
key: ValueKey(likeClickTime),
'x$likeClickTime',
style: TextStyle(
fontSize: 16,
color: theme.brightness.isDark
? theme.primary
: theme.inversePrimary,
},
icon: enableShowDanmaku
? const Icon(
size: 22,
Icons.subtitles_outlined,
color: Color(0xFFEEEEEE),
)
: const Icon(
size: 22,
Icons.subtitles_off_outlined,
color: Color(0xFFEEEEEE),
),
),
);
},
),
const Expanded(
child: Text(
'发送弹幕',
style: TextStyle(color: Color(0xFFEEEEEE)),
),
),
Builder(
builder: (context) {
final theme = Theme.of(context).colorScheme;
return Material(
type: MaterialType.transparency,
child: Stack(
clipBehavior: Clip.none,
children: [
InkWell(
overlayColor: overlayColor(theme),
customBorder: const CircleBorder(),
onTapDown: _liveRoomController.onLikeTapDown,
onTapUp: _liveRoomController.onLikeTapUp,
onTapCancel: _liveRoomController.onLikeTapUp,
child: const SizedBox.square(
dimension: 34,
child: Icon(
size: 22,
color: Color(0xFFEEEEEE),
Icons.thumb_up_off_alt,
),
),
);
}),
),
Positioned(
right: -12,
top: -12,
child: Obx(() {
final likeClickTime =
_liveRoomController.likeClickTime.value;
if (likeClickTime == 0) {
return const SizedBox.shrink();
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 160),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: Text(
key: ValueKey(likeClickTime),
'x$likeClickTime',
style: TextStyle(
fontSize: 16,
color: theme.brightness.isDark
? theme.primary
: theme.inversePrimary,
),
),
);
}),
),
],
),
],
);
},
),
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: IconButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () => onSendDanmaku(true),
icon: const Icon(
size: 22,
color: Color(0xFFEEEEEE),
Icons.emoji_emotions_outlined,
),
),
);
},
),
],
),
ComBtn(
onTap: () => onSendDanmaku(true),
icon: const Icon(
size: 22,
color: Color(0xFFEEEEEE),
Icons.emoji_emotions_outlined,
),
),
],
),
),
),
),
);
);
if (_liveRoomController.showSuperChat) {
return Stack(
children: [
Positioned(
left: 0,
top: 0,
right: 0,
child: Obx(() {
return ClipRect(
clipper: _BorderClipper(
_liveRoomController.pageIndex.value == 0,
),
child: const DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
border: Border(
top: BorderSide(color: Colors.white38),
),
),
child: SizedBox(width: double.infinity, height: 20),
),
);
}),
),
child,
],
);
}
return child;
}
WidgetStateProperty<Color?>? overlayColor(ColorScheme theme) =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
@@ -828,6 +991,27 @@ class _LiveRoomPageState extends State<LiveRoomPage>
}
}
class _BorderClipper extends CustomClipper<Rect> {
_BorderClipper(this.isLeft);
final bool isLeft;
@override
Rect getClip(Size size) {
return Rect.fromLTWH(
isLeft ? 0 : size.width / 2,
0,
size.width / 2,
size.height,
);
}
@override
bool shouldReclip(_BorderClipper oldClipper) {
return isLeft != oldClipper.isLeft;
}
}
class LiveDanmaku extends StatefulWidget {
final LiveRoomController liveRoomController;
final PlPlayerController plPlayerController;