mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-01 15:49:49 +08:00
feat: super chat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user