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,126 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberReportPanel extends StatefulWidget {
const MemberReportPanel({
super.key,
required this.name,
required this.mid,
});
final dynamic name;
final dynamic mid;
@override
State<MemberReportPanel> createState() => _MemberReportPanelState();
}
class _MemberReportPanelState extends State<MemberReportPanel> {
final List<bool> _reasonList = List.generate(3, (_) => false).toList();
final Set<int> _reason = {};
int? _reasonV2;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'举报: ${widget.name}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 4),
Text('uid: ${widget.mid}'),
const SizedBox(height: 10),
const Text('举报内容(必选,可多选)'),
...List.generate(
3,
(index) => _checkBoxWidget(
_reasonList[index],
(value) {
setState(() => _reasonList[index] = value);
if (value) {
_reason.add(index + 1);
} else {
_reason.remove(index + 1);
}
},
['头像违规', '昵称违规', '签名违规'][index],
),
),
const Text('举报理由(单选,非必选)'),
...List.generate(
5,
(index) => RadioWidget<int>(
value: index,
groupValue: _reasonV2,
onChanged: (value) {
setState(() => _reasonV2 = value);
},
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (_reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容');
} else {
Get.back();
dynamic result = await MemberHttp.reportMember(
widget.mid,
reason: _reason.join(','),
reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null,
);
if (result['msg'] is String && result['msg'].isNotEmpty) {
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast('举报失败');
}
}
},
child: const Text('确定'),
),
],
),
],
),
);
}
}
Widget _checkBoxWidget(
bool defValue,
ValueChanged onChanged,
String title,
) {
return InkWell(
onTap: () => onChanged(!defValue),
child: Row(
children: [
Checkbox(
value: defValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
),
);
}

View File

@@ -834,4 +834,15 @@ class Api {
static const String setMsgDnd = static const String setMsgDnd =
'${HttpString.tUrl}/link_setting/v1/link_setting/set_msg_dnd'; '${HttpString.tUrl}/link_setting/v1/link_setting/set_msg_dnd';
static const String imUserInfos = '${HttpString.tUrl}/x/im/user_infos';
static const String getSessionSs =
'${HttpString.tUrl}/link_setting/v1/link_setting/get_session_ss';
static const String getMsgDnd =
'${HttpString.tUrl}/link_setting/v1/link_setting/get_msg_dnd';
static const String setPushSs =
'${HttpString.tUrl}/link_setting/v1/link_setting/set_push_ss';
} }

View File

@@ -6,11 +6,14 @@ import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply/reply_option_type.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
import 'package:PiliPlus/models/msg/account.dart'; import 'package:PiliPlus/models/msg/account.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/msgfeed_at_me.dart'; import 'package:PiliPlus/models/msg/msgfeed_at_me.dart';
import 'package:PiliPlus/models/msg/msgfeed_like_me.dart'; import 'package:PiliPlus/models/msg/msgfeed_like_me.dart';
import 'package:PiliPlus/models/msg/msgfeed_reply_me.dart'; import 'package:PiliPlus/models/msg/msgfeed_reply_me.dart';
import 'package:PiliPlus/models/msg/msgfeed_sys_msg.dart'; import 'package:PiliPlus/models/msg/msgfeed_sys_msg.dart';
import 'package:PiliPlus/models/msg/session.dart'; import 'package:PiliPlus/models/msg/session.dart';
import 'package:PiliPlus/models/msg/session_ss/data.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:PiliPlus/utils/wbi_sign.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -607,4 +610,96 @@ class MsgHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
static Future setPushSs({
required int setting,
required talkerUid,
}) async {
final csrf = Accounts.main.csrf;
var res = await Request().post(
Api.setPushSs,
data: {
'setting': setting,
'talker_uid': talkerUid,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future<LoadingState<List<ImUserInfosData>?>> imUserInfos({
required List uids,
}) async {
final csrf = Accounts.main.csrf;
var res = await Request().get(
Api.imUserInfos,
queryParameters: {
'uids': uids.join(','),
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf,
},
);
if (res.data['code'] == 0) {
return Success((res.data['data'] as List?)
?.map((e) => ImUserInfosData.fromJson(e))
.toList());
} else {
return Error(res.data['message']);
}
}
static Future<LoadingState<SessionSsData>> getSessionSs({
required talkerUid,
}) async {
final csrf = Accounts.main.csrf;
var res = await Request().get(
Api.getSessionSs,
queryParameters: {
'talker_uid': talkerUid,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf,
},
);
if (res.data['code'] == 0) {
return Success(SessionSsData.fromJson(res.data['data']));
} else {
return Error(res.data['message']);
}
}
static Future<LoadingState<List<UidSetting>?>> getMsgDnd({
required uidsStr,
}) async {
final csrf = Accounts.main.csrf;
var res = await Request().get(
Api.getMsgDnd,
queryParameters: {
'own_uid': Accounts.main.mid,
'uids_str': uidsStr,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf,
},
);
if (res.data['code'] == 0) {
return Success((res.data['data']?['uid_settings'] as List?)
?.map((e) => UidSetting.fromJson(e))
.toList());
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -0,0 +1,78 @@
import 'package:PiliPlus/models/model_avatar.dart';
class ImUserInfosData {
int? mid;
String? name;
String? sex;
String? face;
String? sign;
int? rank;
int? level;
int? silence;
Vip? vip;
Pendant? pendant;
BaseOfficialVerify? official;
int? birthday;
int? isFakeAccount;
int? isDeleted;
int? inRegAudit;
int? faceNft;
int? faceNftNew;
int? isSeniorMember;
String? digitalId;
int? digitalType;
ImUserInfosData({
this.mid,
this.name,
this.sex,
this.face,
this.sign,
this.rank,
this.level,
this.silence,
this.vip,
this.pendant,
this.official,
this.birthday,
this.isFakeAccount,
this.isDeleted,
this.inRegAudit,
this.faceNft,
this.faceNftNew,
this.isSeniorMember,
this.digitalId,
this.digitalType,
});
factory ImUserInfosData.fromJson(Map<String, dynamic> json) =>
ImUserInfosData(
mid: json['mid'] as int?,
name: json['name'] as String?,
sex: json['sex'] as String?,
face: json['face'] as String?,
sign: json['sign'] as String?,
rank: json['rank'] as int?,
level: json['level'] as int?,
silence: json['silence'] as int?,
vip: json['vip'] == null
? null
: Vip.fromJson(json['vip'] as Map<String, dynamic>),
pendant: json['pendant'] == null
? null
: Pendant.fromJson(json['pendant'] as Map<String, dynamic>),
official: json['official'] == null
? null
: BaseOfficialVerify.fromJson(
json['official'] as Map<String, dynamic>),
birthday: json['birthday'] as int?,
isFakeAccount: json['is_fake_account'] as int?,
isDeleted: json['is_deleted'] as int?,
inRegAudit: json['in_reg_audit'] as int?,
faceNft: json['face_nft'] as int?,
faceNftNew: json['face_nft_new'] as int?,
isSeniorMember: json['is_senior_member'] as int?,
digitalId: json['digital_id'] as String?,
digitalType: json['digital_type'] as int?,
);
}

View File

@@ -0,0 +1,16 @@
class UidSetting {
int? id;
int? setting;
UidSetting({this.id, this.setting});
factory UidSetting.fromJson(Map<String, dynamic> json) => UidSetting(
id: json['id'] as int?,
setting: json['setting'] as int?,
);
Map<String, dynamic> toJson() => {
'id': id,
'setting': setting,
};
}

View File

@@ -0,0 +1,20 @@
class SessionSsData {
int? followStatus;
int? special;
int? pushSetting;
int? showPushSetting;
SessionSsData({
this.followStatus,
this.special,
this.pushSetting,
this.showPushSetting,
});
factory SessionSsData.fromJson(Map<String, dynamic> json) => SessionSsData(
followStatus: json['follow_status'] as int?,
special: json['special'] as int?,
pushSetting: json['push_setting'] as int?,
showPushSetting: json['show_push_setting'] as int?,
);
}

View File

@@ -1,9 +1,8 @@
import 'package:PiliPlus/common/widgets/dialog/report_member.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar.dart'; import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/space/data.dart'; import 'package:PiliPlus/models/space/data.dart';
import 'package:PiliPlus/pages/member/controller.dart'; import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/pages/member/widget/user_info_card.dart'; import 'package:PiliPlus/pages/member/widget/user_info_card.dart';
@@ -16,7 +15,6 @@ import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MemberPage extends StatefulWidget { class MemberPage extends StatefulWidget {
@@ -137,7 +135,7 @@ class _MemberPageState extends State<MemberPage> {
horizontal: 20, horizontal: 20,
vertical: 16, vertical: 16,
), ),
content: ReportPanel( content: MemberReportPanel(
name: _userController.username, name: _userController.username,
mid: _mid, mid: _mid,
), ),
@@ -290,124 +288,3 @@ class _MemberPageState extends State<MemberPage> {
}; };
} }
} }
class ReportPanel extends StatefulWidget {
const ReportPanel({
super.key,
required this.name,
required this.mid,
});
final dynamic name;
final dynamic mid;
@override
State<ReportPanel> createState() => _ReportPanelState();
}
class _ReportPanelState extends State<ReportPanel> {
final List<bool> _reasonList = List.generate(3, (_) => false).toList();
final Set<int> _reason = {};
int? _reasonV2;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'举报: ${widget.name}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 4),
Text('uid: ${widget.mid}'),
const SizedBox(height: 10),
const Text('举报内容(必选,可多选)'),
...List.generate(
3,
(index) => _checkBoxWidget(
_reasonList[index],
(value) {
setState(() => _reasonList[index] = value);
if (value) {
_reason.add(index + 1);
} else {
_reason.remove(index + 1);
}
},
['头像违规', '昵称违规', '签名违规'][index],
),
),
const Text('举报理由(单选,非必选)'),
...List.generate(
5,
(index) => RadioWidget<int>(
value: index,
groupValue: _reasonV2,
onChanged: (value) {
setState(() => _reasonV2 = value);
},
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (_reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容');
} else {
Get.back();
dynamic result = await MemberHttp.reportMember(
widget.mid,
reason: _reason.join(','),
reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null,
);
if (result['msg'] is String && result['msg'].isNotEmpty) {
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast('举报失败');
}
}
},
child: const Text('确定'),
),
],
),
],
),
);
}
}
Widget _checkBoxWidget(
bool defValue,
ValueChanged onChanged,
String title,
) {
return InkWell(
onTap: () => onChanged(!defValue),
child: Row(
children: [
Checkbox(
value: defValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
),
);
}

View File

@@ -343,11 +343,11 @@ class UserInfoCard extends StatelessWidget {
if (GStorage.userInfo.get('userInfoCache') != null) { if (GStorage.userInfo.get('userInfoCache') != null) {
Get.toNamed( Get.toNamed(
'/whisperDetail', '/whisperDetail',
parameters: { arguments: {
'talkerId': card.mid ?? '', 'talkerId': int.parse(card.mid!),
'name': card.name ?? '', 'name': card.name,
'face': card.face ?? '', 'face': card.face,
'mid': card.mid ?? '', 'mid': card.mid,
}, },
); );
} }

View File

@@ -39,9 +39,13 @@ class _AtMePageState extends State<AtMePage> {
imSettingType: IMSettingType.SETTING_TYPE_OLD_AT_ME), imSettingType: IMSettingType.SETTING_TYPE_OLD_AT_ME),
); );
}, },
icon: const Icon(size: 22, Icons.settings), icon: Icon(
size: 20,
Icons.settings,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 10),
], ],
), ),
body: refreshIndicator( body: refreshIndicator(

View File

@@ -18,6 +18,14 @@ class LikeMeController extends CommonDataController<MsgFeedLikeMe, dynamic> {
queryData(); queryData();
} }
@override
Future<void> queryData([bool isRefresh = true]) {
if (!isRefresh && isEnd) {
return Future.value();
}
return super.queryData(isRefresh);
}
@override @override
bool customHandleResponse(bool isRefresh, Success<MsgFeedLikeMe> response) { bool customHandleResponse(bool isRefresh, Success<MsgFeedLikeMe> response) {
MsgFeedLikeMe data = response.response; MsgFeedLikeMe data = response.response;

View File

@@ -28,6 +28,7 @@ class _LikeMePageState extends State<LikeMePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('收到的赞'), title: const Text('收到的赞'),
@@ -39,9 +40,13 @@ class _LikeMePageState extends State<LikeMePage> {
imSettingType: IMSettingType.SETTING_TYPE_OLD_RECEIVE_LIKE), imSettingType: IMSettingType.SETTING_TYPE_OLD_RECEIVE_LIKE),
); );
}, },
icon: const Icon(size: 22, Icons.settings), icon: Icon(
size: 20,
Icons.settings,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 10),
], ],
), ),
body: refreshIndicator( body: refreshIndicator(
@@ -52,8 +57,8 @@ class _LikeMePageState extends State<LikeMePage> {
SliverPadding( SliverPadding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80), bottom: MediaQuery.paddingOf(context).bottom + 80),
sliver: sliver: Obx(() =>
Obx(() => _buildBody(_likeMeController.loadingState.value)), _buildBody(theme, _likeMeController.loadingState.value)),
), ),
], ],
), ),
@@ -61,7 +66,7 @@ class _LikeMePageState extends State<LikeMePage> {
); );
} }
Widget _buildBody(LoadingState loadingState) { Widget _buildBody(ThemeData theme, LoadingState loadingState) {
return switch (loadingState) { return switch (loadingState) {
Loading() => SliverList.builder( Loading() => SliverList.builder(
itemCount: 12, itemCount: 12,
@@ -70,7 +75,6 @@ class _LikeMePageState extends State<LikeMePage> {
}, },
), ),
Success(:var response) => () { Success(:var response) => () {
final theme = Theme.of(context);
Pair<List<LikeMeItems>, List<LikeMeItems>> pair = response; Pair<List<LikeMeItems>, List<LikeMeItems>> pair = response;
List<LikeMeItems> latest = pair.first; List<LikeMeItems> latest = pair.first;
List<LikeMeItems> total = pair.second; List<LikeMeItems> total = pair.second;

View File

@@ -39,9 +39,13 @@ class _ReplyMePageState extends State<ReplyMePage> {
imSettingType: IMSettingType.SETTING_TYPE_OLD_REPLY_ME), imSettingType: IMSettingType.SETTING_TYPE_OLD_REPLY_ME),
); );
}, },
icon: const Icon(size: 22, Icons.settings), icon: Icon(
size: 20,
Icons.settings,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 10),
], ],
), ),
body: refreshIndicator( body: refreshIndicator(

View File

@@ -576,21 +576,26 @@ class VideoIntroController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
int? mid = videoDetail.value.owner?.mid;
if (mid == null) {
return;
}
int attr = followStatus['attribute'] ?? 0; int attr = followStatus['attribute'] ?? 0;
if (attr == 128) { if (attr == 128) {
dynamic res = await VideoHttp.relationMod( dynamic res = await VideoHttp.relationMod(
mid: videoDetail.value.owner?.mid ?? -1, mid: mid,
act: 6, act: 6,
reSrc: 11, reSrc: 11,
); );
if (res['status']) { if (res['status']) {
GStorage.removeBlackMid(mid);
followStatus['attribute'] = 0; followStatus['attribute'] = 0;
} }
return; return;
} else { } else {
RequestUtils.actionRelationMod( RequestUtils.actionRelationMod(
context: context, context: context,
mid: videoDetail.value.owner?.mid, mid: mid,
isFollow: attr != 0, isFollow: attr != 0,
followStatus: followStatus, followStatus: followStatus,
callback: (attribute) { callback: (attribute) {

View File

@@ -93,13 +93,13 @@ class WhisperSessionItem extends StatelessWidget {
if (item.id.privateId.hasTalkerUid()) { if (item.id.privateId.hasTalkerUid()) {
Get.toNamed( Get.toNamed(
'/whisperDetail', '/whisperDetail',
parameters: { arguments: {
'talkerId': item.id.privateId.talkerUid.toString(), 'talkerId': item.id.privateId.talkerUid.toInt(),
'name': item.sessionInfo.sessionName, 'name': item.sessionInfo.sessionName,
'face': item.sessionInfo.avatar.fallbackLayers.layers.first 'face': item.sessionInfo.avatar.fallbackLayers.layers.first
.resource.resImage.imageSrc.remote.url, .resource.resImage.imageSrc.remote.url,
if (item.sessionInfo.avatar.hasMid()) if (item.sessionInfo.avatar.hasMid())
'mid': item.sessionInfo.avatar.mid.toString(), 'mid': item.sessionInfo.avatar.mid.toInt(),
}, },
); );
return; return;

View File

@@ -19,10 +19,10 @@ import 'package:get/get.dart';
class WhisperDetailController extends CommonListController<RspSessionMsg, Msg> { class WhisperDetailController extends CommonListController<RspSessionMsg, Msg> {
late final ownerMid = Accounts.main.mid; late final ownerMid = Accounts.main.mid;
late int talkerId; final int talkerId = Get.arguments['talkerId'];
late String name; final String name = Get.arguments['name'];
late String face; final String face = Get.arguments['face'];
int? mid; final int? mid = Get.arguments['mid'];
Int64? msgSeqno; Int64? msgSeqno;
@@ -32,14 +32,6 @@ class WhisperDetailController extends CommonListController<RspSessionMsg, Msg> {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
talkerId = int.parse(Get.parameters['talkerId']!);
name = Get.parameters['name']!;
face = Get.parameters['face']!;
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: null;
queryData(); queryData();
} }

View File

@@ -12,6 +12,7 @@ import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/whisper_detail/controller.dart'; import 'package:PiliPlus/pages/whisper_detail/controller.dart';
import 'package:PiliPlus/pages/whisper_detail/widget/chat_item.dart'; import 'package:PiliPlus/pages/whisper_detail/widget/chat_item.dart';
import 'package:PiliPlus/pages/whisper_link_setting/view.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
@@ -95,6 +96,21 @@ class _WhisperDetailPageState
], ],
), ),
), ),
actions: [
IconButton(
onPressed: () {
Get.to(WhisperLinkSettingPage(
talkerUid: _whisperDetailController.talkerId,
));
},
icon: Icon(
size: 20,
Icons.settings,
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.8),
),
),
const SizedBox(width: 10),
],
), ),
body: SafeArea( body: SafeArea(
top: false, top: false,

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,
),
),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'
show SelectItem, Setting, SettingSwitch; show SelectItem, Setting, SettingSwitch;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -120,43 +122,55 @@ class ImSettingsItem extends StatelessWidget {
if (item.hasSelect()) { if (item.hasSelect()) {
String? selected; String? selected;
late final divider = Divider(
height: 1,
indent: 16,
color: outline.withOpacity(0.1),
);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: item.select.item.map((e) { children: List.generate(
if (e.selected) { max(0, item.select.item.length * 2 - 1),
selected ??= e.text; (index) {
} if (index.isOdd) {
return ListTile( return divider;
dense: true, }
onTap: () async { final e = item.select.item[index ~/ 2];
if (!e.selected) { if (e.selected) {
for (var i in item.select.item) { selected ??= e.text;
i.selected = false; }
} return ListTile(
e.selected = true; dense: true,
rebuild(); onTap: () async {
if (!e.selected) {
if (await onSet()) {
selected = e.text;
} else {
for (var i in item.select.item) { for (var i in item.select.item) {
i.selected = i.text == selected; i.selected = false;
} }
e.selected = true;
rebuild(); rebuild();
if (await onSet()) {
selected = e.text;
} else {
for (var i in item.select.item) {
i.selected = i.text == selected;
}
rebuild();
}
} }
} },
}, title: Text(e.text, style: titleStyle),
title: Text(e.text, style: titleStyle), trailing: e.selected
trailing: e.selected ? Icon(
? Icon( size: 20,
size: 20, Icons.check,
Icons.check, color: theme.colorScheme.primary,
color: theme.colorScheme.primary, )
) : null,
: null, );
); },
}).toList(), ),
); );
} }

View File

@@ -172,8 +172,8 @@ extension ThreeDotItemTypeExt on ThreeDotItemType {
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_UP_HELPER: case ThreeDotItemType.THREE_DOT_ITEM_TYPE_UP_HELPER:
Get.toNamed( Get.toNamed(
'/whisperDetail', '/whisperDetail',
parameters: { arguments: {
'talkerId': '844424930131966', 'talkerId': 844424930131966,
'name': 'UP主小助手', 'name': 'UP主小助手',
'face': 'face':
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png', 'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',

View File

@@ -496,6 +496,10 @@ class GStorage {
GStorage.localCache.put(LocalCacheKey.blackMids, blackMids..add(mid)); GStorage.localCache.put(LocalCacheKey.blackMids, blackMids..add(mid));
} }
static void removeBlackMid(int mid) {
GStorage.localCache.put(LocalCacheKey.blackMids, blackMids..remove(mid));
}
static MemberTabType get memberTab => MemberTabType static MemberTabType get memberTab => MemberTabType
.values[setting.get(SettingBoxKey.memberTab, defaultValue: 0)]; .values[setting.get(SettingBoxKey.memberTab, defaultValue: 0)];