From 0bc0c36f14e93fb6d317767de343627987330fd7 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Thu, 19 Jun 2025 18:46:01 +0800 Subject: [PATCH] feat: live dm block Signed-off-by: bggRGjQaUbCoE --- ...tom_sliver_persistent_header_delegate.dart | 28 +- lib/http/api.dart | 15 + lib/http/live.dart | 106 ++++++ lib/models/common/live_dm_silent_type.dart | 1 + lib/models_new/live/live_dm_block/data.dart | 16 + .../live/live_dm_block/shield_info.dart | 31 ++ .../live/live_dm_block/shield_rules.dart | 13 + .../live/live_dm_block/shield_user_list.dart | 13 + lib/pages/dynamics/widgets/module_panel.dart | 15 +- lib/pages/live_dm_block/controller.dart | 151 ++++++++ lib/pages/live_dm_block/view.dart | 356 ++++++++++++++++++ .../live_room/widgets/bottom_control.dart | 30 ++ .../live_room/widgets/header_control.dart | 1 + lib/router/app_pages.dart | 3 + 14 files changed, 760 insertions(+), 19 deletions(-) create mode 100644 lib/models/common/live_dm_silent_type.dart create mode 100644 lib/models_new/live/live_dm_block/data.dart create mode 100644 lib/models_new/live/live_dm_block/shield_info.dart create mode 100644 lib/models_new/live/live_dm_block/shield_rules.dart create mode 100644 lib/models_new/live/live_dm_block/shield_user_list.dart create mode 100644 lib/pages/live_dm_block/controller.dart create mode 100644 lib/pages/live_dm_block/view.dart diff --git a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart index 6f4da515..e54f024b 100644 --- a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart +++ b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart @@ -11,7 +11,7 @@ class CustomSliverPersistentHeaderDelegate final double _minExtent; final double _maxExtent; final Widget child; - final Color bgColor; + final Color? bgColor; @override Widget build( @@ -19,18 +19,20 @@ class CustomSliverPersistentHeaderDelegate //创建child子组件 //shrinkOffset:child偏移值minExtent~maxExtent //overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false - return DecoratedBox( - decoration: BoxDecoration( - color: bgColor, - boxShadow: [ - BoxShadow( - color: bgColor, - offset: const Offset(0, -2), - ), - ], - ), - child: child, - ); + return bgColor != null + ? DecoratedBox( + decoration: BoxDecoration( + color: bgColor, + boxShadow: [ + BoxShadow( + color: bgColor!, + offset: const Offset(0, -2), + ), + ], + ), + child: child, + ) + : child; } //SliverPersistentHeader最大高度 diff --git a/lib/http/api.dart b/lib/http/api.dart index 200d4b70..25c68910 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -896,4 +896,19 @@ class Api { static const String dynPic = '/x/polymer/web-dynamic/v1/detail/pic'; static const String msgLikeDetail = '/x/msgfeed/like_detail'; + + static const String getLiveInfoByUser = + '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getInfoByUser'; + + static const String liveSetSilent = + '${HttpString.liveBaseUrl}/liveact/user_silent'; + + static const String addShieldKeyword = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/v1/banned/AddShieldKeyword'; + + static const String delShieldKeyword = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/v1/banned/DelShieldKeyword'; + + static const String liveShieldUser = + '${HttpString.liveBaseUrl}/liveact/shield_user'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 869617af..d9d7131f 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -5,6 +5,8 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/live_search_type.dart'; import 'package:PiliPlus/models_new/live/live_area_list/area_item.dart'; import 'package:PiliPlus/models_new/live/live_area_list/area_list.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/data.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_info.dart'; import 'package:PiliPlus/models_new/live/live_dm_info/data.dart'; import 'package:PiliPlus/models_new/live/live_emote/data.dart'; import 'package:PiliPlus/models_new/live/live_emote/datum.dart'; @@ -453,4 +455,108 @@ class LiveHttp { return Error(res.data['message']); } } + + static Future> getLiveInfoByUser( + dynamic roomId) async { + var res = await Request().get( + Api.getLiveInfoByUser, + queryParameters: await WbiSign.makSign({ + 'room_id': roomId, + 'from': 0, + 'not_mock_enter_effect': 1, + 'web_location': 444.8, + }), + ); + if (res.data['code'] == 0) { + return Success(LiveDmBlockData.fromJson(res.data['data']).shieldInfo); + } else { + return Error(res.data['message']); + } + } + + static Future liveSetSilent({ + required String type, + required int level, + }) async { + final csrf = Accounts.main.csrf; + var res = await Request().post( + Api.liveSetSilent, + data: { + 'type': type, + 'level': level, + 'csrf': csrf, + 'csrf_token': csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future addShieldKeyword({ + required String keyword, + }) async { + final csrf = Accounts.main.csrf; + var res = await Request().post( + Api.addShieldKeyword, + data: { + 'keyword': keyword, + 'csrf': csrf, + 'csrf_token': csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future delShieldKeyword({ + required String keyword, + }) async { + final csrf = Accounts.main.csrf; + var res = await Request().post( + Api.delShieldKeyword, + data: { + 'keyword': keyword, + 'csrf': csrf, + 'csrf_token': csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future liveShieldUser({ + required dynamic uid, + required dynamic roomid, + required int type, + }) async { + final csrf = Accounts.main.csrf; + var res = await Request().post( + Api.liveShieldUser, + data: { + 'uid': uid, + 'roomid': roomid, + 'type': type, + 'csrf': csrf, + 'csrf_token': csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/models/common/live_dm_silent_type.dart b/lib/models/common/live_dm_silent_type.dart new file mode 100644 index 00000000..f0b1a291 --- /dev/null +++ b/lib/models/common/live_dm_silent_type.dart @@ -0,0 +1 @@ +enum LiveDmSilentType { level, rank, verify } diff --git a/lib/models_new/live/live_dm_block/data.dart b/lib/models_new/live/live_dm_block/data.dart new file mode 100644 index 00000000..a81d31d6 --- /dev/null +++ b/lib/models_new/live/live_dm_block/data.dart @@ -0,0 +1,16 @@ +import 'package:PiliPlus/models_new/live/live_dm_block/shield_info.dart'; + +class LiveDmBlockData { + ShieldInfo? shieldInfo; + + LiveDmBlockData({ + this.shieldInfo, + }); + + factory LiveDmBlockData.fromJson(Map json) => + LiveDmBlockData( + shieldInfo: json['shield_info'] == null + ? null + : ShieldInfo.fromJson(json['shield_info'] as Map), + ); +} diff --git a/lib/models_new/live/live_dm_block/shield_info.dart b/lib/models_new/live/live_dm_block/shield_info.dart new file mode 100644 index 00000000..52f62515 --- /dev/null +++ b/lib/models_new/live/live_dm_block/shield_info.dart @@ -0,0 +1,31 @@ +import 'package:PiliPlus/models_new/live/live_dm_block/shield_rules.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; + +class ShieldInfo { + List? shieldUserList; + List? keywordList; + ShieldRules? shieldRules; + bool? isBlock; + int? blockExpired; + + ShieldInfo({ + this.shieldUserList, + this.keywordList, + this.shieldRules, + this.isBlock, + this.blockExpired, + }); + + factory ShieldInfo.fromJson(Map json) => ShieldInfo( + shieldUserList: (json['shield_user_list'] as List?) + ?.map((e) => ShieldUserList.fromJson(e as Map)) + .toList(), + keywordList: (json['keyword_list'] as List?)?.cast(), + shieldRules: json['shield_rules'] == null + ? null + : ShieldRules.fromJson( + json['shield_rules'] as Map), + isBlock: json['is_block'] as bool?, + blockExpired: json['block_expired'] as int?, + ); +} diff --git a/lib/models_new/live/live_dm_block/shield_rules.dart b/lib/models_new/live/live_dm_block/shield_rules.dart new file mode 100644 index 00000000..a32ba093 --- /dev/null +++ b/lib/models_new/live/live_dm_block/shield_rules.dart @@ -0,0 +1,13 @@ +class ShieldRules { + int rank; + int verify; + int level; + + ShieldRules({this.rank = 0, this.verify = 0, this.level = 0}); + + factory ShieldRules.fromJson(Map json) => ShieldRules( + rank: json['rank'] as int? ?? 0, + verify: json['verify'] as int? ?? 0, + level: json['level'] as int? ?? 0, + ); +} diff --git a/lib/models_new/live/live_dm_block/shield_user_list.dart b/lib/models_new/live/live_dm_block/shield_user_list.dart new file mode 100644 index 00000000..ce125cf9 --- /dev/null +++ b/lib/models_new/live/live_dm_block/shield_user_list.dart @@ -0,0 +1,13 @@ +class ShieldUserList { + int? uid; + String? uname; + + ShieldUserList({this.uid, this.uname}); + + factory ShieldUserList.fromJson(Map json) { + return ShieldUserList( + uid: json['uid'] as int?, + uname: json['uname'] as String?, + ); + } +} diff --git a/lib/pages/dynamics/widgets/module_panel.dart b/lib/pages/dynamics/widgets/module_panel.dart index 7d8d494f..01292fa3 100644 --- a/lib/pages/dynamics/widgets/module_panel.dart +++ b/lib/pages/dynamics/widgets/module_panel.dart @@ -225,12 +225,15 @@ Widget module( if (item.modules.moduleDynamic!.major!.common!.cover ?.isNotEmpty == true) - CachedNetworkImage( - width: 45, - height: 45, - fit: BoxFit.cover, - imageUrl: item.modules.moduleDynamic!.major!.common!.cover! - .http2https, + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: CachedNetworkImage( + width: 45, + height: 45, + fit: BoxFit.cover, + imageUrl: item.modules.moduleDynamic!.major!.common! + .cover!.http2https, + ), ), Expanded( child: Column( diff --git a/lib/pages/live_dm_block/controller.dart b/lib/pages/live_dm_block/controller.dart new file mode 100644 index 00000000..72cafc2a --- /dev/null +++ b/lib/pages/live_dm_block/controller.dart @@ -0,0 +1,151 @@ +import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/models/common/live_dm_silent_type.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_info.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_rules.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class LiveDmBlockController extends GetxController + with GetSingleTickerProviderStateMixin { + final roomId = Get.parameters['roomId']; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + late final TabController tabController = + TabController(length: 2, vsync: this); + + int? oldLevel; + final RxInt level = 0.obs; + final RxInt rank = 0.obs; + final RxInt verify = 0.obs; + final RxBool isEnable = false.obs; + + final RxList keywordList = [].obs; + final RxList shieldUserList = [].obs; + + void updateValue() { + isEnable.value = level.value != 0 || rank.value != 0 || verify.value != 0; + } + + Future queryData() async { + var res = await LiveHttp.getLiveInfoByUser(roomId); + if (res.isSuccess) { + ShieldInfo? data = res.data; + ShieldRules? shieldRules = data?.shieldRules; + level.value = shieldRules?.level ?? 0; + rank.value = shieldRules?.rank ?? 0; + verify.value = shieldRules?.verify ?? 0; + updateValue(); + + if (data?.keywordList != null) { + keywordList.addAll(data!.keywordList!); + } + if (data?.shieldUserList != null) { + shieldUserList.addAll(data!.shieldUserList!); + } + } else { + res.toast(); + } + } + + Future setSilent(LiveDmSilentType type, int level, + {VoidCallback? onError}) async { + var res = await LiveHttp.liveSetSilent(type: type.name, level: level); + if (res['status']) { + switch (type) { + case LiveDmSilentType.level: + this.level.value = level; + case LiveDmSilentType.rank: + rank.value = level; + case LiveDmSilentType.verify: + verify.value = level; + } + updateValue(); + return true; + } else { + onError?.call(); + SmartDialog.showToast(res['msg']); + return false; + } + } + + Future setEnable(bool enable) async { + if (enable == isEnable.value) { + return; + } + final futures = enable + ? [ + setSilent(LiveDmSilentType.rank, 1), + setSilent(LiveDmSilentType.verify, 1), + ] + : [ + for (var e in LiveDmSilentType.values) setSilent(e, 0), + ]; + var res = await Future.wait(futures); + if (enable) { + if (res.any((e) => e)) { + isEnable.value = true; + } + } else { + if (res.every((e) => e)) { + isEnable.value = false; + } + } + } + + Future addShieldKeyword(bool isKeyword, String value) async { + if (isKeyword) { + var res = await LiveHttp.addShieldKeyword(keyword: value); + if (res['status']) { + keywordList.insert(0, value); + } else { + SmartDialog.showToast(res['msg']); + } + } else { + var res = + await LiveHttp.liveShieldUser(uid: value, roomid: roomId, type: 1); + if (res['status']) { + shieldUserList.insert( + 0, + ShieldUserList( + uid: res['data']['uid'], + uname: res['data']['uname'], + ), + ); + } else { + SmartDialog.showToast(res['msg']); + } + } + } + + Future onRemove(int index, dynamic item) async { + if (item is ShieldUserList) { + var res = + await LiveHttp.liveShieldUser(uid: item.uid, roomid: roomId, type: 0); + if (res['status']) { + shieldUserList.removeAt(index); + } else { + SmartDialog.showToast(res['msg']); + } + } else { + var res = await LiveHttp.delShieldKeyword(keyword: item); + if (res['status']) { + keywordList.removeAt(index); + } else { + SmartDialog.showToast(res['msg']); + } + } + } + + @override + void onClose() { + tabController.dispose(); + super.onClose(); + } +} diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart new file mode 100644 index 00000000..b714bca8 --- /dev/null +++ b/lib/pages/live_dm_block/view.dart @@ -0,0 +1,356 @@ +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/models/common/live_dm_silent_type.dart'; +import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; +import 'package:PiliPlus/pages/live_dm_block/controller.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class LiveDmBlockPage extends StatefulWidget { + const LiveDmBlockPage({super.key}); + + @override + State createState() => _LiveDmBlockPageState(); +} + +class _LiveDmBlockPageState extends State { + final _controller = + Get.put(LiveDmBlockController(), tag: Utils.generateRandomString(8)); + late bool isPortrait; + + @override + Widget build(BuildContext context) { + isPortrait = context.orientation == Orientation.portrait; + final theme = Theme.of(context); + + Widget tabBar = TabBar( + controller: _controller.tabController, + tabs: const [Tab(text: '关键词'), Tab(text: '用户')], + ); + + Widget view = tabBarView( + controller: _controller.tabController, + children: [ + KeepAliveWrapper( + builder: (context) => + Obx(() => _buildKeyword(_controller.keywordList)), + ), + KeepAliveWrapper( + builder: (context) => + Obx(() => _buildKeyword(_controller.shieldUserList)), + ), + ], + ); + + Widget title = Padding( + padding: EdgeInsets.only( + top: isPortrait ? 18 : 0, left: isPortrait ? 0 : 12, bottom: 12), + child: const Text( + '关键词屏蔽', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold), + ), + ); + + Widget left = Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '全局屏蔽', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold), + ), + ..._buildHeader(theme), + if (isPortrait) title, + ], + ), + ); + + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: const Text('弹幕屏蔽'), + ), + body: SafeArea( + bottom: false, + child: Stack( + clipBehavior: Clip.none, + children: [ + isPortrait + ? ExtendedNestedScrollView( + onlyOneScrollInBody: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverToBoxAdapter(child: left), + SliverOverlapAbsorber( + handle: ExtendedNestedScrollView + .sliverOverlapAbsorberHandleFor(context), + sliver: SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 48, + child: tabBar, + bgColor: null, + ), + ), + ), + ]; + }, + body: LayoutBuilder( + builder: (context, _) { + return Padding( + padding: EdgeInsets.only( + top: ExtendedNestedScrollView + .sliverOverlapAbsorberHandleFor(context) + .layoutExtent ?? + 0, + ), + child: view, + ); + }, + ), + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: left), + VerticalDivider( + width: 1, + color: theme.colorScheme.outline.withValues(alpha: 0.1), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isPortrait) title, + tabBar, + Expanded(child: view) + ], + ), + ), + ], + ), + Positioned( + right: 16, + bottom: 16 + MediaQuery.paddingOf(context).bottom, + child: FloatingActionButton( + tooltip: '添加', + onPressed: _addShieldKeyword, + child: const Icon(Icons.add), + ), + ), + ], + ), + ), + ); + } + + Widget _buildKeyword(List list) { + if (list.isEmpty) { + return isPortrait ? errorWidget() : scrollErrorWidget(); + } + return SingleChildScrollView( + padding: EdgeInsets.only( + top: 12, + left: 12, + right: 12, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + child: Wrap( + spacing: 12, + runSpacing: 12, + children: list.indexed.map( + (e) { + final item = e.$2; + return SearchText( + text: item is ShieldUserList ? item.uname! : item as String, + onTap: (value) => showConfirmDialog( + context: context, + title: '确定删除该规则?', + onConfirm: () => _controller.onRemove(e.$1, item), + ), + ); + }, + ).toList(), + ), + ); + } + + List _buildHeader(ThemeData theme) { + return [ + const SizedBox(height: 6), + Obx( + () => Row( + spacing: 10, + children: [ + Text('屏蔽${_controller.isEnable.value ? '已' : '未'}开启'), + Transform.scale( + scale: .8, + child: Switch( + value: _controller.isEnable.value, + onChanged: _controller.setEnable, + ), + ), + ], + ), + ), + const SizedBox(height: 6), + Obx( + () { + return Row( + children: [ + const Text('用户等级'), + Slider( + min: 0, + max: 60, + // ignore: deprecated_member_use + year2023: true, + inactiveColor: theme.colorScheme.onInverseSurface, + padding: const EdgeInsets.only(left: 20, right: 25), + value: _controller.level.value.toDouble(), + onChangeStart: (value) => + _controller.oldLevel = _controller.level.value, + onChanged: (value) => + _controller.level.value = value.round().clamp(0, 60), + onChangeEnd: (value) { + if (_controller.oldLevel != _controller.level.value) { + _controller.setSilent( + LiveDmSilentType.level, + _controller.level.value, + onError: () => + _controller.level.value = _controller.oldLevel ?? 0, + ); + } + }, + ), + Text('${_controller.level.value} 以下') + ], + ); + }, + ), + const SizedBox(height: 20), + Row( + spacing: 16, + children: [ + Obx(() { + final isEnable = _controller.rank.value == 1; + return _headerBtn( + theme, + isEnable, + Icons.live_tv, + '非正式会员', + () => _controller.setSilent( + LiveDmSilentType.rank, + isEnable ? 0 : 1, + ), + ); + }), + Obx(() { + final isEnable = _controller.verify.value == 1; + return _headerBtn( + theme, + isEnable, + Icons.smartphone, + '未绑定手机用户', + () => _controller.setSilent( + LiveDmSilentType.verify, + isEnable ? 0 : 1, + ), + ); + }), + ], + ), + ]; + } + + Widget _headerBtn(ThemeData theme, bool isEnable, IconData icon, String name, + VoidCallback onTap) { + final color = + isEnable ? theme.colorScheme.primary : theme.colorScheme.outline; + + Widget top = Container( + width: 42, + height: 42, + alignment: Alignment.center, + decoration: isEnable + ? BoxDecoration( + border: Border.all(color: color), + borderRadius: const BorderRadius.all(Radius.circular(4))) + : null, + child: Icon(icon, color: color), + ); + + if (isEnable) { + top = Stack( + clipBehavior: Clip.none, + children: [ + top, + Positioned( + right: -6, + top: -6, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.error, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(2), + child: Icon( + size: 14, + Icons.horizontal_rule, + color: theme.colorScheme.onError, + ), + ), + ), + ), + ], + ); + } + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Column( + spacing: 5, + children: [ + top, + Text( + name, + style: TextStyle(color: color), + ), + ], + ), + ); + } + + void _addShieldKeyword() { + bool isKeyword = _controller.tabController.index == 0; + String value = ''; + showConfirmDialog( + context: context, + title: '${isKeyword ? '关键词' : '用户'}屏蔽', + content: TextFormField( + autofocus: true, + initialValue: value, + onChanged: (val) => value = val, + decoration: isKeyword ? null : const InputDecoration(hintText: 'UID'), + keyboardType: isKeyword ? null : TextInputType.number, + inputFormatters: isKeyword + ? null + : [FilteringTextInputFormatter.allow(RegExp(r'\d+'))], + ), + onConfirm: () { + if (value.isNotEmpty) { + _controller.addShieldKeyword(isKeyword, value); + } + }, + ); + } +} diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 98c84d39..9c90fa49 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class BottomControl extends StatelessWidget { @@ -46,11 +47,40 @@ class BottomControl extends StatelessWidget { onTap: onRefresh, ), const Spacer(), + SizedBox( + width: 35, + height: 35, + child: IconButton( + tooltip: '弹幕屏蔽', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + if (liveRoomCtr.accountService.isLogin.value) { + Get.toNamed( + '/liveDmBlockPage', + parameters: { + 'roomId': liveRoomCtr.roomId.toString(), + }, + ); + } else { + SmartDialog.showToast('账号未登录'); + } + }, + icon: const Icon( + size: 18, + Icons.block, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 10), Obx( () => SizedBox( width: 35, height: 35, child: IconButton( + tooltip: '弹幕开关', style: ButtonStyle( padding: WidgetStateProperty.all(EdgeInsets.zero), ), diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index c2735522..e823e5e8 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -147,6 +147,7 @@ class LiveHeaderControl extends StatelessWidget { width: 35, height: 35, child: IconButton( + tooltip: '定时关闭', style: ButtonStyle( padding: WidgetStateProperty.all(EdgeInsets.zero), ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d44df556..dbb59360 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -20,6 +20,7 @@ import 'package:PiliPlus/pages/home/view.dart'; import 'package:PiliPlus/pages/hot/view.dart'; import 'package:PiliPlus/pages/later/view.dart'; import 'package:PiliPlus/pages/later_search/view.dart'; +import 'package:PiliPlus/pages/live_dm_block/view.dart'; import 'package:PiliPlus/pages/live_room/view.dart'; import 'package:PiliPlus/pages/login/view.dart'; import 'package:PiliPlus/pages/main/view.dart'; @@ -183,6 +184,8 @@ class Routes { CustomGetPage(name: '/dynTopicRcmd', page: () => const DynTopicRcmdPage()), CustomGetPage(name: '/matchInfo', page: () => const MatchInfoPage()), CustomGetPage(name: '/msgLikeDetail', page: () => const LikeDetailPage()), + CustomGetPage( + name: '/liveDmBlockPage', page: () => const LiveDmBlockPage()), ]; }