feat: msg link setting

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-12 17:43:28 +08:00
parent 964668c982
commit 9a97a5d110
21 changed files with 925 additions and 189 deletions

View File

@@ -0,0 +1,179 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/dialog/report_member.dart';
import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart';
import 'package:PiliPlus/grpc/im.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/msg/im_user_infos/datum.dart';
import 'package:PiliPlus/models/msg/msg_dnd/uid_setting.dart';
import 'package:PiliPlus/models/msg/session_ss/data.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class WhisperLinkSettingController extends GetxController {
WhisperLinkSettingController({
required this.talkerUid,
});
final int talkerUid;
RxBool isPinned = false.obs;
late final sessionId =
SessionId(privateId: PrivateId(talkerUid: Int64(talkerUid)));
@override
void onInit() {
super.onInit();
getUserInfo();
getSessionSs();
getMsgDnd();
getIsPinned();
}
final Rx<LoadingState<List<ImUserInfosData>?>> userState =
LoadingState<List<ImUserInfosData>?>.loading().obs;
final Rx<LoadingState<SessionSsData>> sessionSs =
LoadingState<SessionSsData>.loading().obs;
final Rx<LoadingState<List<UidSetting>?>> msgDnd =
LoadingState<List<UidSetting>?>.loading().obs;
Future<void> getUserInfo() async {
userState.value = await MsgHttp.imUserInfos(uids: [talkerUid]);
}
Future<void> getSessionSs() async {
sessionSs.value = await MsgHttp.getSessionSs(talkerUid: talkerUid);
}
Future<void> getMsgDnd() async {
msgDnd.value = await MsgHttp.getMsgDnd(uidsStr: talkerUid);
}
Future<void> getIsPinned() async {
var res = await ImGrpc.sessionUpdate(sessionId: sessionId);
if (res.isSuccess) {
isPinned.value = res.data.session.isPinned;
}
}
void setPush(bool isPush) {
if (isPush) {
showConfirmDialog(
context: Get.context!,
title: '确认关闭内容推送吗?',
content: '若关闭此开关,你将不再收到该账号的图文消息与稿件推送,但通知类消息不受影响',
onConfirm: () => _setPush(isPush),
);
return;
}
_setPush(isPush);
}
Future<void> _setPush(bool isPush) async {
int setting = isPush ? 1 : 0;
var res = await MsgHttp.setPushSs(
setting: setting,
talkerUid: talkerUid,
);
if (res['status']) {
sessionSs
..value.data.pushSetting = setting
..refresh();
SmartDialog.showToast('操作成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
Future<void> setPin() async {
var res = isPinned.value
? await ImGrpc.unpinSession(sessionId: sessionId)
: await ImGrpc.pinSession(sessionId: sessionId);
if (res.isSuccess) {
isPinned.value = !isPinned.value;
SmartDialog.showToast('操作成功');
} else {
res.toast();
}
}
Future<void> setMute(bool isMuted) async {
int setting = isMuted ? 0 : 1;
var res = await MsgHttp.setMsgDnd(
uid: Accounts.main.mid,
setting: setting,
dndUid: talkerUid,
);
if (res['status']) {
msgDnd
..value.data!.first.setting = setting
..refresh();
SmartDialog.showToast('操作成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
Future<void> setBlock(bool isBlocked) async {
if (isBlocked) {
var res = await VideoHttp.relationMod(
mid: talkerUid,
act: 6,
reSrc: 11,
);
if (res['status']) {
sessionSs
..value.data.followStatus = null
..refresh();
GStorage.removeBlackMid(talkerUid);
SmartDialog.showToast('操作成功');
} else {
SmartDialog.showToast(res['msg']);
}
} else {
showConfirmDialog(
context: Get.context!,
title: '确认拉黑该用户',
content: '加入黑名单后,将自动解除关注关系和对该用户的合集订阅关系,禁止该用户与我互动或查看我的空间',
onConfirm: () async {
var res = await VideoHttp.relationMod(
mid: talkerUid,
act: 5,
reSrc: 11,
);
if (res['status']) {
sessionSs
..value.data.followStatus = 128
..refresh();
GStorage.setBlackMid(talkerUid);
SmartDialog.showToast('操作成功');
} else {
SmartDialog.showToast(res['msg']);
}
},
);
}
}
void report() {
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
content: MemberReportPanel(
name: userState.value.dataOrNull?.firstOrNull?.name ?? '',
mid: talkerUid,
),
),
);
}
}

View File

@@ -0,0 +1,283 @@
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/msg/im_user_infos/datum.dart';
import 'package:PiliPlus/models/msg/msg_dnd/uid_setting.dart';
import 'package:PiliPlus/models/msg/session_ss/data.dart';
import 'package:PiliPlus/pages/whisper_link_setting/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class WhisperLinkSettingPage extends StatefulWidget {
const WhisperLinkSettingPage({
super.key,
required this.talkerUid,
});
final int talkerUid;
@override
State<WhisperLinkSettingPage> createState() => _WhisperLinkSettingPageState();
}
class _WhisperLinkSettingPageState extends State<WhisperLinkSettingPage> {
late final WhisperLinkSettingController _controller = Get.put(
WhisperLinkSettingController(talkerUid: widget.talkerUid),
tag: Utils.generateRandomString(8),
);
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final divider = Divider(
height: 12,
thickness: 12,
color: theme.colorScheme.outline.withOpacity(0.1),
);
final divider2 = Divider(
height: 1,
indent: 16,
color: theme.colorScheme.outline.withOpacity(0.1),
);
return Scaffold(
appBar: AppBar(title: const Text('聊天设置')),
body: ListView(
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 80),
children: [
divider,
Obx(() =>
_buildUserInfo(theme, divider, _controller.userState.value)),
Obx(() => _buildSessionSs(
theme, divider, divider2, _controller.sessionSs.value)),
Obx(() => _controller.sessionSs.value.isSuccess
? _buildBlockItem(
_controller.sessionSs.value.data.followStatus == 128)
: const SizedBox.shrink()),
divider2,
ListTile(
dense: true,
onTap: _controller.report,
title: const Text('举报', style: TextStyle(fontSize: 14)),
trailing: Icon(
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
),
divider,
],
),
);
}
Widget _buildBlockItem(bool isBlocked) {
return ListTile(
dense: true,
onTap: () => _controller.setBlock(isBlocked),
title: const Text('加入黑名单', style: TextStyle(fontSize: 14)),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon:
WidgetStateProperty.resolveWith<Icon?>((Set<WidgetState> states) {
if (states.isNotEmpty && states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: isBlocked,
onChanged: (value) => _controller.setBlock(isBlocked),
),
),
);
}
Widget _buildUserInfo(
ThemeData theme,
Widget divider,
LoadingState<List<ImUserInfosData>?> loadingState,
) {
return switch (loadingState) {
Loading() => const SizedBox.shrink(),
Success(:var response) => response?.isNotEmpty == true
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Builder(
builder: (context) {
final ImUserInfosData item = response!.first;
return ListTile(
onTap: () {
Get.toNamed('/member?mid=${item.mid}');
},
leading: PendantAvatar(
avatar: item.face,
size: 42,
badgeSize: 14,
isVip: item.vip?.status != null && item.vip!.status > 0,
garbPendantImage: item.pendant?.image,
officialType: item.official?.type,
),
title: Text(
item.name!,
style: TextStyle(
fontSize: 14,
color: item.vip?.status != null &&
item.vip!.status > 0 &&
item.vip?.type == 2
? context.vipColor
: null,
),
),
subtitle: Text(
'UID: ${item.mid}${item.sign?.isNotEmpty == true ? '\n${item.sign}' : ''}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
trailing: Icon(
size: 22,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
);
},
),
divider,
],
)
: const SizedBox.shrink(),
Error(:var errMsg) => _errWidget(errMsg, _controller.getUserInfo),
};
}
Widget _buildSessionSs(
ThemeData theme,
Widget divider,
Widget divider2,
LoadingState<SessionSsData> loadingState,
) {
return switch (loadingState) {
Loading() => const SizedBox.shrink(),
Success(:var response) => Builder(
builder: (context) {
late final subTitleS =
TextStyle(fontSize: 13, color: theme.colorScheme.outline);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (response.showPushSetting == 1)
ListTile(
dense: true,
onTap: () => _controller.setPush(response.pushSetting == 0),
title: const Text('接收消息推送', style: TextStyle(fontSize: 14)),
subtitle: Text(
'若关闭此开关,你将不再收到该账号的图文消息与稿件推送,但通知类消息不受影响',
style: subTitleS,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: response.pushSetting == 0,
onChanged: (value) =>
_controller.setPush(response.pushSetting == 0),
),
),
),
divider2,
Obx(
() => ListTile(
dense: true,
onTap: _controller.setPin,
title: const Text('置顶聊天', style: TextStyle(fontSize: 14)),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: _controller.isPinned.value,
onChanged: (value) => _controller.setPin(),
),
),
),
),
divider2,
Obx(() => _buildMuteItem(_controller.msgDnd.value)),
divider,
],
);
},
),
Error(:var errMsg) => _errWidget(errMsg, _controller.getSessionSs),
};
}
Widget _buildMuteItem(LoadingState<List<UidSetting>?> loadingState) {
return switch (loadingState) {
Loading() => const SizedBox.shrink(),
Success(:var response) => response?.isNotEmpty == true
? ListTile(
dense: true,
onTap: () => _controller.setMute(response.first.setting == 1),
title: const Text('消息免打扰', style: TextStyle(fontSize: 14)),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: response!.first.setting == 1,
onChanged: (value) =>
_controller.setMute(response.first.setting == 1),
),
),
)
: const SizedBox.shrink(),
Error(:var errMsg) => _errWidget(errMsg, _controller.getMsgDnd),
};
}
Widget _errWidget(String? errMsg, VoidCallback onTap) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
errMsg ?? '',
textAlign: TextAlign.center,
),
),
);
}
}