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

@@ -47,6 +47,7 @@
## feat ## feat
- [x] SuperChat
- [x] 播放课堂视频 - [x] 播放课堂视频
- [x] 发起投票 - [x] 发起投票
- [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户` - [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户`

View File

@@ -962,4 +962,7 @@ class Api {
static const String spaceShop = static const String spaceShop =
'${HttpString.mallBaseUrl}/community-hub/small_shop/feed/tab/item'; '${HttpString.mallBaseUrl}/community-hub/small_shop/feed/tab/item';
static const String superChatMsg =
'${HttpString.liveBaseUrl}/av/v1/SuperChat/getMessageList';
} }

View File

@@ -18,6 +18,7 @@ import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
import 'package:PiliPlus/models_new/live/live_room_play_info/data.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/data.dart';
import 'package:PiliPlus/models_new/live/live_search/data.dart'; import 'package:PiliPlus/models_new/live/live_search/data.dart';
import 'package:PiliPlus/models_new/live/live_second_list/data.dart'; import 'package:PiliPlus/models_new/live/live_second_list/data.dart';
import 'package:PiliPlus/models_new/live/live_superchat/data.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/app_sign.dart';
import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:PiliPlus/utils/wbi_sign.dart';
@@ -642,4 +643,24 @@ class LiveHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
static Future<LoadingState<SuperChatData>> superChatMsg(
dynamic roomId,
) async {
var res = await Request().get(
Api.superChatMsg,
queryParameters: {
'room_id': roomId,
},
);
if (res.data['code'] == 0) {
try {
return Success(SuperChatData.fromJson(res.data['data']));
} catch (e) {
return Error(e.toString());
}
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -0,0 +1,13 @@
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
class SuperChatData {
List<SuperChatItem>? list;
SuperChatData({this.list});
factory SuperChatData.fromJson(Map<String, dynamic> json) => SuperChatData(
list: (json['list'] as List<dynamic>?)
?.map((e) => SuperChatItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}

View File

@@ -0,0 +1,42 @@
import 'package:PiliPlus/models_new/live/live_superchat/user_info.dart';
import 'package:PiliPlus/utils/utils.dart';
class SuperChatItem {
dynamic id;
dynamic uid;
int? price;
String backgroundColor;
String backgroundBottomColor;
String backgroundPriceColor;
String messageFontColor;
int endTime;
String message;
UserInfo userInfo;
bool expired = false;
SuperChatItem({
this.id,
required this.uid,
this.price,
required this.backgroundColor,
required this.backgroundBottomColor,
required this.backgroundPriceColor,
required this.messageFontColor,
required this.endTime,
required this.message,
required this.userInfo,
});
factory SuperChatItem.fromJson(Map<String, dynamic> json) => SuperChatItem(
id: json['id'] ?? Utils.generateRandomString(8),
uid: json['uid'],
price: json['price'] as int?,
backgroundColor: json['background_color'] ?? '#EDF5FF',
backgroundBottomColor: json['background_bottom_color'] ?? '#2A60B2',
backgroundPriceColor: json['background_price_color'] ?? '#7497CD',
messageFontColor: json['message_font_color'] ?? '#FFFFFF',
endTime: json['end_time'],
message: json['message'],
userInfo: UserInfo.fromJson(json['user_info'] as Map<String, dynamic>),
);
}

View File

@@ -0,0 +1,17 @@
class UserInfo {
String face;
String uname;
String nameColor;
UserInfo({
required this.face,
required this.uname,
required this.nameColor,
});
factory UserInfo.fromJson(Map<String, dynamic> json) => UserInfo(
face: json['face'],
uname: json['uname'],
nameColor: json['name_color'] ?? '#666666',
);
}

View File

@@ -10,7 +10,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
import 'package:PiliPlus/pages/dynamics_mention/controller.dart'; import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
import 'package:PiliPlus/pages/dynamics_mention/widgets/item.dart'; import 'package:PiliPlus/pages/dynamics_mention/widgets/item.dart';
import 'package:PiliPlus/pages/search/controller.dart' show SearchState; import 'package:PiliPlus/pages/search/controller.dart' show DebounceStreamState;
import 'package:PiliPlus/utils/context_ext.dart'; import 'package:PiliPlus/utils/context_ext.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -58,7 +58,8 @@ class DynMentionPanel extends StatefulWidget {
State<DynMentionPanel> createState() => _DynMentionPanelState(); State<DynMentionPanel> createState() => _DynMentionPanelState();
} }
class _DynMentionPanelState extends SearchState<DynMentionPanel> { class _DynMentionPanelState
extends DebounceStreamState<DynMentionPanel, String> {
final _controller = Get.put(DynMentionController()); final _controller = Get.put(DynMentionController());
@override @override
Duration get duration => const Duration(milliseconds: 300); Duration get duration => const Duration(milliseconds: 300);

View File

@@ -8,7 +8,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart'; import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart'; import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart';
import 'package:PiliPlus/pages/search/controller.dart' show SearchState; import 'package:PiliPlus/pages/search/controller.dart' show DebounceStreamState;
import 'package:PiliPlus/utils/context_ext.dart'; import 'package:PiliPlus/utils/context_ext.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -56,7 +56,8 @@ class SelectTopicPanel extends StatefulWidget {
State<SelectTopicPanel> createState() => _SelectTopicPanelState(); State<SelectTopicPanel> createState() => _SelectTopicPanelState();
} }
class _SelectTopicPanelState extends SearchState<SelectTopicPanel> { class _SelectTopicPanelState
extends DebounceStreamState<SelectTopicPanel, String> {
final _controller = Get.put(SelectTopicController()); final _controller = Get.put(SelectTopicController());
@override @override
Duration get duration => const Duration(milliseconds: 300); Duration get duration => const Duration(milliseconds: 300);

View File

@@ -12,6 +12,7 @@ import 'package:PiliPlus/models_new/live/live_dm_info/data.dart';
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
import 'package:PiliPlus/models_new/live/live_room_play_info/data.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/data.dart';
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart';
import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/services/service_locator.dart';
@@ -64,10 +65,14 @@ class LiveRoomController extends GetxController {
LiveDmInfoData? dmInfo; LiveDmInfoData? dmInfo;
List<RichTextItem>? savedDanmaku; List<RichTextItem>? savedDanmaku;
RxList<DanmakuMsg> messages = <DanmakuMsg>[].obs; RxList<DanmakuMsg> messages = <DanmakuMsg>[].obs;
late final Rx<SuperChatItem?> fsSC = Rx<SuperChatItem?>(null);
late final RxList<SuperChatItem> superChatMsg = <SuperChatItem>[].obs;
RxBool disableAutoScroll = false.obs; RxBool disableAutoScroll = false.obs;
LiveMessageStream? _msgStream; LiveMessageStream? _msgStream;
late final ScrollController scrollController = ScrollController() late final ScrollController scrollController = ScrollController()
..addListener(listener); ..addListener(listener);
late final RxInt pageIndex = 0.obs;
PageController? pageController;
int? currentQn; int? currentQn;
RxString currentQnDesc = ''.obs; RxString currentQnDesc = ''.obs;
@@ -81,6 +86,8 @@ class LiveRoomController extends GetxController {
bool? isPlaying; bool? isPlaying;
late bool isFullScreen = false; late bool isFullScreen = false;
final showSuperChat = Pref.showSuperChat;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -92,6 +99,9 @@ class LiveRoomController extends GetxController {
if (isLogin && !Pref.historyPause) { if (isLogin && !Pref.historyPause) {
VideoHttp.roomEntryAction(roomId: roomId); VideoHttp.roomEntryAction(roomId: roomId);
} }
if (showSuperChat) {
pageController = PageController();
}
} }
Future<void>? playerInit({bool autoplay = true}) { Future<void>? playerInit({bool autoplay = true}) {
@@ -205,27 +215,50 @@ class LiveRoomController extends GetxController {
} }
} }
void jumpToBottom() {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
}
void closeLiveMsg() { void closeLiveMsg() {
_msgStream?.close(); _msgStream?.close();
_msgStream = null; _msgStream = null;
} }
Future<void> prefetch() async {
final res = await LiveHttp.liveRoomDanmaPrefetch(roomId: roomId);
if (res['status']) {
if (res['data'] case List list) {
try {
messages.addAll(
list.cast<Map<String, dynamic>>().map(DanmakuMsg.fromPrefetch),
);
WidgetsBinding.instance.addPostFrameCallback(scrollToBottom);
} catch (e) {
if (kDebugMode) debugPrint(e.toString());
}
}
}
}
Future<void> getSuperChatMsg() async {
final res = await LiveHttp.superChatMsg(roomId);
if (res.dataOrNull?.list case List<SuperChatItem> list) {
superChatMsg.addAll(list);
}
}
void clearSC() {
superChatMsg.removeWhere((e) => e.expired);
}
void startLiveMsg() { void startLiveMsg() {
if (messages.isEmpty) { if (messages.isEmpty) {
LiveHttp.liveRoomDanmaPrefetch(roomId: roomId).then((v) { prefetch();
if (v['status']) { if (showSuperChat) {
if (v['data'] case List list) { getSuperChatMsg();
try { }
messages.addAll(
list.cast<Map<String, dynamic>>().map(DanmakuMsg.fromPrefetch),
);
WidgetsBinding.instance.addPostFrameCallback(scrollToBottom);
} catch (e) {
if (kDebugMode) debugPrint(e.toString());
}
}
}
});
} }
if (_msgStream != null) { if (_msgStream != null) {
return; return;
@@ -257,14 +290,20 @@ class LiveRoomController extends GetxController {
@override @override
void onClose() { void onClose() {
closeLiveMsg();
cancelLikeTimer(); cancelLikeTimer();
cancelLiveTimer(); cancelLiveTimer();
savedDanmaku?.clear(); savedDanmaku?.clear();
savedDanmaku = null; savedDanmaku = null;
closeLiveMsg(); messages.clear();
if (showSuperChat) {
superChatMsg.clear();
fsSC.value = null;
}
scrollController scrollController
..removeListener(listener) ..removeListener(listener)
..dispose(); ..dispose();
pageController?.dispose();
super.onClose(); super.onClose();
} }
@@ -294,49 +333,63 @@ class LiveRoomController extends GetxController {
) )
..addEventListener((obj) { ..addEventListener((obj) {
try { try {
if (obj['cmd'] == 'DANMU_MSG') { // logger.i(' 原始弹幕消息 ======> ${jsonEncode(obj)}');
// logger.i(' 原始弹幕消息 ======> ${jsonEncode(obj)}'); switch (obj['cmd']) {
final info = obj['info']; case 'DANMU_MSG':
final first = info[0]; final info = obj['info'];
final content = first[15]; final first = info[0];
final extra = jsonDecode(content['extra']); final content = first[15];
final user = content['user']; final extra = jsonDecode(content['extra']);
final uid = user['uid']; final user = content['user'];
messages.add( final uid = user['uid'];
DanmakuMsg() BaseEmote? uemote;
..name = user['base']['name'] if (first[13] case Map<String, dynamic> map) {
..uid = uid uemote = BaseEmote.fromJson(map);
..text = info[1] }
..emots = (extra['emots'] as Map<String, dynamic>?)?.map( messages.add(
(k, v) => MapEntry(k, BaseEmote.fromJson(v)), DanmakuMsg(
) name: user['base']['name'],
..uemote = first[13] is Map<String, dynamic> uid: uid,
? BaseEmote.fromJson(first[13]) text: info[1],
: null, emots: (extra['emots'] as Map<String, dynamic>?)?.map(
); (k, v) => MapEntry(k, BaseEmote.fromJson(v)),
),
if (plPlayerController.showDanmaku) { uemote: uemote,
plPlayerController.danmakuController?.addDanmaku(
DanmakuContentItem(
extra['content'],
color: DmUtils.decimalToColor(extra['color']),
type: DmUtils.getPosition(extra['mode']),
selfSend: isLogin && uid == mid,
), ),
); );
if (!disableAutoScroll.value) {
EasyThrottle.throttle( if (plPlayerController.showDanmaku) {
'liveDm', plPlayerController.danmakuController?.addDanmaku(
const Duration(milliseconds: 500), DanmakuContentItem(
() => WidgetsBinding.instance.addPostFrameCallback( extra['content'],
scrollToBottom, color: DmUtils.decimalToColor(extra['color']),
type: DmUtils.getPosition(extra['mode']),
selfSend: extra['send_from_me'] ?? false,
), ),
); );
if (!disableAutoScroll.value) {
EasyThrottle.throttle(
'liveDm',
const Duration(milliseconds: 500),
() => WidgetsBinding.instance.addPostFrameCallback(
scrollToBottom,
),
);
}
} }
} break;
case 'SUPER_CHAT_MESSAGE' when (showSuperChat):
final item = SuperChatItem.fromJson(obj['data']);
superChatMsg.insert(0, item);
if (isFullScreen) {
fsSC.value = item;
}
break;
} }
} catch (e) { } catch (e) {
if (kDebugMode) debugPrint(e.toString()); if (kDebugMode) {
debugPrint('$e,,$obj');
}
} }
}) })
..init(); ..init();

View File

@@ -0,0 +1,149 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SuperChatCard extends StatefulWidget {
const SuperChatCard({
super.key,
required this.item,
required this.onRemove,
});
final SuperChatItem item;
final VoidCallback onRemove;
@override
State<SuperChatCard> createState() => _SuperChatCardState();
}
class _SuperChatCardState extends State<SuperChatCard> {
Timer? _timer;
RxInt _remains = 0.obs;
@override
void initState() {
super.initState();
if (widget.item.expired) {
_onRemove();
return;
}
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final offset = widget.item.endTime - now;
if (offset > 0) {
_remains = offset.obs;
_timer = Timer.periodic(const Duration(seconds: 1), _callback);
} else {
_onRemove();
}
}
void _onRemove() {
widget
..item.expired = true
..onRemove();
}
void _callback(_) {
final remains = _remains.value;
if (remains > 0) {
_remains.value = remains - 1;
} else {
_cancelTimer();
_onRemove();
}
}
void _cancelTimer() {
_timer?.cancel();
_timer = null;
}
@override
void dispose() {
_cancelTimer();
super.dispose();
}
@override
Widget build(BuildContext context) {
final item = widget.item;
final bottomColor = Utils.parseColor(item.backgroundBottomColor);
final border = BorderSide(color: bottomColor);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
color: Utils.parseColor(item.backgroundColor),
border: Border(top: border, left: border, right: border),
),
padding: const EdgeInsets.all(8),
child: Row(
spacing: 12,
children: [
GestureDetector(
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
child: NetworkImgLayer(
src: item.userInfo.face,
width: 45,
height: 45,
type: ImageType.avatar,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.userInfo.uname,
style: TextStyle(
color: Utils.parseColor(item.userInfo.nameColor),
),
),
Text(
"${item.price}",
style: TextStyle(
fontSize: 12,
color: Utils.parseColor(item.backgroundPriceColor),
),
),
],
),
),
Obx(
() => Text(
_remains.toString(),
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
),
],
),
),
Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
color: bottomColor,
),
padding: const EdgeInsets.all(8),
child: SelectableText(
item.message,
style: TextStyle(color: Utils.parseColor(item.messageFontColor)),
),
),
],
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:PiliPlus/pages/live_room/controller.dart';
import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart';
import 'package:PiliPlus/pages/search/controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
class SuperChatPanel extends StatefulWidget {
const SuperChatPanel({
super.key,
required this.controller,
});
final LiveRoomController controller;
@override
State<SuperChatPanel> createState() => _SuperChatPanelState();
}
class _SuperChatPanelState extends DebounceStreamState<SuperChatPanel, bool> {
@override
Duration get duration => const Duration(milliseconds: 300);
@override
Widget build(BuildContext context) {
return Obx(
() => ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12),
physics: const ClampingScrollPhysics(),
itemCount: widget.controller.superChatMsg.length,
itemBuilder: (context, index) {
final item = widget.controller.superChatMsg[index];
return SuperChatCard(
key: Key(item.id.toString()),
item: item,
onRemove: () => ctr?.add(true),
);
},
separatorBuilder: (_, _) => const SizedBox(height: 12),
),
);
}
@override
void onValueChanged(value) => widget.controller.clearSC();
}

View File

@@ -1,19 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; 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/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/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.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/controller.dart';
import 'package:PiliPlus/pages/live_room/send_danmaku/view.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/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/pages/live_room/widgets/header_control.dart';
import 'package:PiliPlus/plugin/pl_player/controller.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/models/play_status.dart';
import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart';
import 'package:PiliPlus/plugin/pl_player/view.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/services/service_locator.dart';
import 'package:PiliPlus/utils/duration_util.dart'; import 'package:PiliPlus/utils/duration_util.dart';
import 'package:PiliPlus/utils/extension.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:cached_network_image/cached_network_image.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemUiOverlayStyle; import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -44,8 +50,10 @@ class _LiveRoomPageState extends State<LiveRoomPage>
late final PlPlayerController plPlayerController; late final PlPlayerController plPlayerController;
bool get isFullScreen => plPlayerController.isFullScreen.value; bool get isFullScreen => plPlayerController.isFullScreen.value;
final GlobalKey chatKey = GlobalKey(); late final GlobalKey pageKey = GlobalKey();
final GlobalKey playerKey = GlobalKey(); late final GlobalKey chatKey = GlobalKey();
late final GlobalKey scKey = GlobalKey();
late final GlobalKey playerKey = GlobalKey();
@override @override
void initState() { void initState() {
@@ -193,7 +201,128 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Alignment? alignment, Alignment? alignment,
bool needDm = true, bool needDm = true,
}) { }) {
if (!isFullScreen) {
_liveRoomController.fsSC.value = null;
}
_liveRoomController.isFullScreen = isFullScreen; _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( return PopScope(
canPop: !isFullScreen, canPop: !isFullScreen,
onPopInvokedWithResult: (bool didPop, Object? result) { onPopInvokedWithResult: (bool didPop, Object? result) {
@@ -201,40 +330,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
plPlayerController.triggerFullScreen(status: false); plPlayerController.triggerFullScreen(status: false);
} }
}, },
child: Obx(() { child: player,
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();
}),
); );
} }
@@ -344,7 +440,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Widget get _buildPP { Widget get _buildPP {
final isFullScreen = this.isFullScreen; final isFullScreen = this.isFullScreen;
final bottomHeight = 80.0 + padding.bottom; final bottomHeight = 70 + padding.bottom;
final topPadding = padding.top + kToolbarHeight; final topPadding = padding.top + kToolbarHeight;
final videoHeight = maxHeight - bottomHeight - topPadding; final videoHeight = maxHeight - bottomHeight - topPadding;
return Stack( return Stack(
@@ -562,7 +658,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Widget get _buildBodyH { Widget get _buildBodyH {
final videoWidth = 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; final videoHeight = maxHeight - padding.top;
return Obx( return Obx(
() { () {
@@ -619,148 +715,215 @@ class _LiveRoomPageState extends State<LiveRoomPage>
], ],
); );
Widget _buildChatWidget([bool isPP = false]) => Padding( Widget _buildChatWidget([bool isPP = false]) {
padding: EdgeInsets.only(bottom: 16, top: !isPortrait ? 0 : 16), Widget chat() => LiveRoomChatPanel(
child: LiveRoomChat(
key: chatKey, key: chatKey,
isPP: isPP, isPP: isPP,
roomId: _liveRoomController.roomId, roomId: _liveRoomController.roomId,
liveRoomController: _liveRoomController, 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( Widget get _buildInputWidget {
padding: EdgeInsets.only( final child = Container(
top: 5, padding: EdgeInsets.only(
left: 10, top: 5,
right: 10, left: 10,
bottom: 15 + padding.bottom, right: 10,
), bottom: padding.bottom,
height: 80 + padding.bottom,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
), ),
border: Border( height: 70 + padding.bottom,
top: BorderSide(color: Color(0x1AFFFFFF)), 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,
child: GestureDetector( behavior: HitTestBehavior.opaque,
onTap: onSendDanmaku, child: Padding(
behavior: HitTestBehavior.opaque, padding: const EdgeInsets.only(top: 5, bottom: 10),
child: Padding( child: Align(
padding: const EdgeInsets.only(top: 5, bottom: 10), alignment: Alignment.topCenter,
child: Row( child: Row(
spacing: 6, spacing: 6,
children: [ children: [
Obx( Obx(
() { () {
final enableShowDanmaku = final enableShowDanmaku =
plPlayerController.enableShowDanmaku.value; plPlayerController.enableShowDanmaku.value;
return ComBtn( return SizedBox(
onTap: () { width: 34,
final newVal = !enableShowDanmaku; height: 34,
plPlayerController.enableShowDanmaku.value = newVal; child: IconButton(
if (!plPlayerController.tempPlayerConf) { style: IconButton.styleFrom(
GStorage.setting.put( padding: EdgeInsets.zero,
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),
), ),
); onPressed: () {
}, final newVal = !enableShowDanmaku;
), plPlayerController.enableShowDanmaku.value = newVal;
const Expanded( if (!plPlayerController.tempPlayerConf) {
child: Text( GStorage.setting.put(
'发送弹幕', SettingBoxKey.enableShowDanmaku,
style: TextStyle(color: Color(0xFFEEEEEE)), newVal,
), );
),
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), icon: enableShowDanmaku
transitionBuilder: (child, animation) { ? const Icon(
return ScaleTransition( size: 22,
scale: animation, Icons.subtitles_outlined,
child: child, color: Color(0xFFEEEEEE),
); )
}, : const Icon(
child: Text( size: 22,
key: ValueKey(likeClickTime), Icons.subtitles_off_outlined,
'x$likeClickTime', color: Color(0xFFEEEEEE),
style: TextStyle( ),
fontSize: 16, ),
color: theme.brightness.isDark );
? theme.primary },
: theme.inversePrimary, ),
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<Color?>? overlayColor(ColorScheme theme) =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) { 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 { class LiveDanmaku extends StatefulWidget {
final LiveRoomController liveRoomController; final LiveRoomController liveRoomController;
final PlPlayerController plPlayerController; final PlPlayerController plPlayerController;

View File

@@ -1,14 +1,16 @@
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_danmaku/danmaku_msg.dart'; import 'package:PiliPlus/models_new/live/live_danmaku/danmaku_msg.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/controller.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class LiveRoomChat extends StatelessWidget { class LiveRoomChatPanel extends StatelessWidget {
const LiveRoomChat({ const LiveRoomChatPanel({
super.key, super.key,
required this.roomId, required this.roomId,
required this.liveRoomController, required this.liveRoomController,
@@ -33,24 +35,23 @@ class LiveRoomChat extends StatelessWidget {
children: [ children: [
Obx( Obx(
() => ListView.separated( () => ListView.separated(
padding: EdgeInsets.zero, padding: const EdgeInsets.symmetric(horizontal: 12),
controller: liveRoomController.scrollController, controller: liveRoomController.scrollController,
separatorBuilder: (context, index) => const SizedBox(height: 6), separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: liveRoomController.messages.length, itemCount: liveRoomController.messages.length,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = liveRoomController.messages[index]; final item = liveRoomController.messages[index];
return Container( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, horizontal: 10,
vertical: 5, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: const BorderRadius.all(Radius.circular(18)), borderRadius: const BorderRadius.all(Radius.circular(14)),
), ),
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
@@ -61,14 +62,11 @@ class LiveRoomChat extends StatelessWidget {
color: nameColor, color: nameColor,
fontSize: 14, fontSize: 14,
), ),
recognizer: TapGestureRecognizer() recognizer: item.uid == 0
..onTap = () { ? null
try { : (TapGestureRecognizer()
Get.toNamed('/member?mid=${item.uid}'); ..onTap = () =>
} catch (err) { Get.toNamed('/member?mid=${item.uid}')),
if (kDebugMode) debugPrint(err.toString());
}
},
), ),
_buildMsg(devicePixelRatio, item), _buildMsg(devicePixelRatio, item),
], ],
@@ -79,12 +77,101 @@ class LiveRoomChat extends StatelessWidget {
}, },
), ),
), ),
if (kDebugMode && liveRoomController.showSuperChat) ...[
Positioned(
top: 50,
right: 0,
child: TextButton(
onPressed: () {
liveRoomController.superChatMsg.insert(
0,
SuperChatItem.fromJson({
"id": Utils.generateRandomString(8),
"price": 66,
"end_time":
DateTime.now().millisecondsSinceEpoch ~/ 1000 + 5,
"message": "message message message message message",
"user_info": {
"face": "",
"uname": "UNAME",
},
}),
);
},
child: const Text('add superchat'),
),
),
Positioned(
right: 0,
top: 90,
child: TextButton(
onPressed: () {
if (liveRoomController.superChatMsg.isNotEmpty) {
liveRoomController.superChatMsg.removeLast();
}
},
child: const Text('remove superchat'),
),
),
],
if (liveRoomController.showSuperChat)
Positioned(
top: 12,
right: 12,
child: Obx(() {
final isEmpty = liveRoomController.superChatMsg.isEmpty;
return AnimatedOpacity(
opacity: isEmpty ? 0 : 1,
duration: const Duration(milliseconds: 120),
child: GestureDetector(
onTap: isEmpty
? null
: () => liveRoomController.pageController?.animateToPage(
1,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: const Color(0x2FFFFFFF),
border: Border.all(color: Colors.white24, width: 0.7),
),
padding: const EdgeInsets.fromLTRB(10, 4, 4, 4),
child: Text.rich(
style: const TextStyle(color: Colors.white),
strutStyle: const StrutStyle(height: 1, leading: 0),
TextSpan(
children: [
TextSpan(
text:
'SC: ${liveRoomController.superChatMsg.length}',
),
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
size: 18,
Icons.keyboard_arrow_right,
color: Colors.white,
),
),
],
),
),
),
),
);
}),
),
Obx( Obx(
() => liveRoomController.disableAutoScroll.value () => liveRoomController.disableAutoScroll.value
? Positioned( ? Positioned(
right: 12, right: 12,
bottom: 0, bottom: 0,
child: ElevatedButton.icon( child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.comfortable,
),
icon: const Icon( icon: const Icon(
Icons.arrow_downward_rounded, Icons.arrow_downward_rounded,
size: 20, size: 20,
@@ -92,7 +179,7 @@ class LiveRoomChat extends StatelessWidget {
label: const Text('回到底部'), label: const Text('回到底部'),
onPressed: () => liveRoomController onPressed: () => liveRoomController
..disableAutoScroll.value = false ..disableAutoScroll.value = false
..scrollToBottom(), ..jumpToBottom(),
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),

View File

@@ -474,15 +474,13 @@ class UserInfoCard extends StatelessWidget {
bool isLight, bool isLight,
SpacePrInfo prInfo, SpacePrInfo prInfo,
) { ) {
final textColor = !isLight final textColor = Utils.parseColor(
? Color(int.parse('FF${prInfo.textColorNight.substring(1)}', radix: 16)) isLight ? prInfo.textColor : prInfo.textColorNight,
: Color(int.parse('FF${prInfo.textColor.substring(1)}', radix: 16)); );
Widget child = Container( Widget child = Container(
margin: const EdgeInsets.only(top: 8), margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
color: !isLight color: Utils.parseColor(isLight ? prInfo.bgColor : prInfo.bgColorNight),
? Color(int.parse('FF${prInfo.bgColorNight.substring(1)}', radix: 16))
: Color(int.parse('FF${prInfo.bgColor.substring(1)}', radix: 16)),
child: Row( child: Row(
children: [ children: [
if (!isLight && prInfo.iconNight?.isNotEmpty == true) ...[ if (!isLight && prInfo.iconNight?.isNotEmpty == true) ...[

View File

@@ -33,8 +33,8 @@ mixin DebounceStreamMixin<T> {
} }
} }
abstract class SearchState<T extends StatefulWidget> extends State<T> abstract class DebounceStreamState<T extends StatefulWidget, S> extends State<T>
with DebounceStreamMixin<String> { with DebounceStreamMixin<S> {
@override @override
void dispose() { void dispose() {
subDispose(); subDispose();

View File

@@ -129,6 +129,13 @@ List<SettingsModel> get playSettings => [
} }
}, },
), ),
const SettingsModel(
settingsType: SettingsType.sw1tch,
title: '显示 SuperChat',
leading: Icon(Icons.live_tv),
setKey: SettingBoxKey.showSuperChat,
defaultVal: true,
),
const SettingsModel( const SettingsModel(
settingsType: SettingsType.sw1tch, settingsType: SettingsType.sw1tch,
title: '竖屏扩大展示', title: '竖屏扩大展示',

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/pages/search/controller.dart' show SearchState; import 'package:PiliPlus/pages/search/controller.dart' show DebounceStreamState;
import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; import 'package:PiliPlus/pages/setting/models/extra_settings.dart';
import 'package:PiliPlus/pages/setting/models/model.dart'; import 'package:PiliPlus/pages/setting/models/model.dart';
import 'package:PiliPlus/pages/setting/models/play_settings.dart'; import 'package:PiliPlus/pages/setting/models/play_settings.dart';
@@ -22,7 +22,8 @@ class SettingsSearchPage extends StatefulWidget {
State<SettingsSearchPage> createState() => _SettingsSearchPageState(); State<SettingsSearchPage> createState() => _SettingsSearchPageState();
} }
class _SettingsSearchPageState extends SearchState<SettingsSearchPage> { class _SettingsSearchPageState
extends DebounceStreamState<SettingsSearchPage, String> {
final _textEditingController = TextEditingController(); final _textEditingController = TextEditingController();
final RxList<SettingsModel> _list = <SettingsModel>[].obs; final RxList<SettingsModel> _list = <SettingsModel>[].obs;
late final _settings = [ late final _settings = [
@@ -91,19 +92,24 @@ class _SettingsSearchPageState extends SearchState<SettingsSearchPage> {
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
ViewSliverSafeArea( ViewSliverSafeArea(
sliver: Obx( sliver: MediaQuery.removeViewPadding(
() => _list.isEmpty context: context,
? const HttpError() removeLeft: true,
: SliverWaterfallFlow( removeRight: true,
gridDelegate: child: Obx(
SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( () => _list.isEmpty
maxCrossAxisExtent: Grid.smallCardWidth * 2, ? const HttpError()
), : SliverWaterfallFlow(
delegate: SliverChildBuilderDelegate( gridDelegate:
(_, index) => _list[index].widget, SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
childCount: _list.length, maxCrossAxisExtent: Grid.smallCardWidth * 2,
),
delegate: SliverChildBuilderDelegate(
(_, index) => _list[index].widget,
childCount: _list.length,
),
), ),
), ),
), ),
), ),
], ],

View File

@@ -23,7 +23,8 @@ class SettingBoxKey {
enableAutoBrightness = 'enableAutoBrightness', enableAutoBrightness = 'enableAutoBrightness',
enableAutoEnter = 'enableAutoEnter', enableAutoEnter = 'enableAutoEnter',
enableAutoExit = 'enableAutoExit', enableAutoExit = 'enableAutoExit',
enableOnlineTotal = 'enableOnlineTotal'; enableOnlineTotal = 'enableOnlineTotal',
showSuperChat = 'showSuperChat';
static const String enableVerticalExpand = 'enableVerticalExpand', static const String enableVerticalExpand = 'enableVerticalExpand',
feedBackEnable = 'feedBackEnable', feedBackEnable = 'feedBackEnable',

View File

@@ -807,4 +807,7 @@ class Pref {
static bool get showMemberShop => static bool get showMemberShop =>
_setting.get(SettingBoxKey.showMemberShop, defaultValue: false); _setting.get(SettingBoxKey.showMemberShop, defaultValue: false);
static bool get showSuperChat =>
_setting.get(SettingBoxKey.showSuperChat, defaultValue: true);
} }

View File

@@ -15,6 +15,9 @@ class Utils {
static const channel = MethodChannel("PiliPlus"); static const channel = MethodChannel("PiliPlus");
static Color parseColor(String color) =>
Color(int.parse(color.replaceFirst('#', 'FF'), radix: 16));
static int? _sdkInt; static int? _sdkInt;
static Future<int> get sdkInt async { static Future<int> get sdkInt async {