mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +08:00
feat: live dm block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
151
lib/pages/live_dm_block/controller.dart
Normal file
151
lib/pages/live_dm_block/controller.dart
Normal file
@@ -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<String> keywordList = <String>[].obs;
|
||||
final RxList<ShieldUserList> shieldUserList = <ShieldUserList>[].obs;
|
||||
|
||||
void updateValue() {
|
||||
isEnable.value = level.value != 0 || rank.value != 0 || verify.value != 0;
|
||||
}
|
||||
|
||||
Future<void> 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<bool> 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<void> 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<void> 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<void> 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();
|
||||
}
|
||||
}
|
||||
356
lib/pages/live_dm_block/view.dart
Normal file
356
lib/pages/live_dm_block/view.dart
Normal file
@@ -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<LiveDmBlockPage> createState() => _LiveDmBlockPageState();
|
||||
}
|
||||
|
||||
class _LiveDmBlockPageState extends State<LiveDmBlockPage> {
|
||||
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<Widget> _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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user