From 9a97a5d110b381044fcaed72c957929e35b92efa Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Mon, 12 May 2025 17:43:28 +0800 Subject: [PATCH] feat: msg link setting Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/dialog/report_member.dart | 126 ++++++++ lib/http/api.dart | 11 + lib/http/msg.dart | 95 ++++++ lib/models/msg/im_user_infos/datum.dart | 78 +++++ lib/models/msg/msg_dnd/uid_setting.dart | 16 + lib/models/msg/session_ss/data.dart | 20 ++ lib/pages/member/view.dart | 127 +------- lib/pages/member/widget/user_info_card.dart | 10 +- lib/pages/msg_feed_top/at_me/view.dart | 8 +- .../msg_feed_top/like_me/controller.dart | 8 + lib/pages/msg_feed_top/like_me/view.dart | 16 +- lib/pages/msg_feed_top/reply_me/view.dart | 8 +- .../video/introduction/ugc/controller.dart | 9 +- lib/pages/whisper/widgets/item.dart | 6 +- lib/pages/whisper_detail/controller.dart | 16 +- lib/pages/whisper_detail/view.dart | 16 + .../whisper_link_setting/controller.dart | 179 +++++++++++ lib/pages/whisper_link_setting/view.dart | 283 ++++++++++++++++++ lib/pages/whisper_settings/widgets/item.dart | 74 +++-- lib/utils/extension.dart | 4 +- lib/utils/storage.dart | 4 + 21 files changed, 925 insertions(+), 189 deletions(-) create mode 100644 lib/common/widgets/dialog/report_member.dart create mode 100644 lib/models/msg/im_user_infos/datum.dart create mode 100644 lib/models/msg/msg_dnd/uid_setting.dart create mode 100644 lib/models/msg/session_ss/data.dart create mode 100644 lib/pages/whisper_link_setting/controller.dart create mode 100644 lib/pages/whisper_link_setting/view.dart diff --git a/lib/common/widgets/dialog/report_member.dart b/lib/common/widgets/dialog/report_member.dart new file mode 100644 index 00000000..af4a5b06 --- /dev/null +++ b/lib/common/widgets/dialog/report_member.dart @@ -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 createState() => _MemberReportPanelState(); +} + +class _MemberReportPanelState extends State { + final List _reasonList = List.generate(3, (_) => false).toList(); + final Set _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( + 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), + ], + ), + ); +} diff --git a/lib/http/api.dart b/lib/http/api.dart index dce369b8..6b58e7cb 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -834,4 +834,15 @@ class Api { static const String setMsgDnd = '${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'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 5eedfa19..da83825a 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -6,11 +6,14 @@ import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.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_like_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/session.dart'; +import 'package:PiliPlus/models/msg/session_ss/data.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:dio/dio.dart'; @@ -607,4 +610,96 @@ class MsgHttp { 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?>> 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> 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?>> 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']); + } + } } diff --git a/lib/models/msg/im_user_infos/datum.dart b/lib/models/msg/im_user_infos/datum.dart new file mode 100644 index 00000000..0f5955ef --- /dev/null +++ b/lib/models/msg/im_user_infos/datum.dart @@ -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 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), + pendant: json['pendant'] == null + ? null + : Pendant.fromJson(json['pendant'] as Map), + official: json['official'] == null + ? null + : BaseOfficialVerify.fromJson( + json['official'] as Map), + 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?, + ); +} diff --git a/lib/models/msg/msg_dnd/uid_setting.dart b/lib/models/msg/msg_dnd/uid_setting.dart new file mode 100644 index 00000000..16455ad7 --- /dev/null +++ b/lib/models/msg/msg_dnd/uid_setting.dart @@ -0,0 +1,16 @@ +class UidSetting { + int? id; + int? setting; + + UidSetting({this.id, this.setting}); + + factory UidSetting.fromJson(Map json) => UidSetting( + id: json['id'] as int?, + setting: json['setting'] as int?, + ); + + Map toJson() => { + 'id': id, + 'setting': setting, + }; +} diff --git a/lib/models/msg/session_ss/data.dart b/lib/models/msg/session_ss/data.dart new file mode 100644 index 00000000..b1d9b299 --- /dev/null +++ b/lib/models/msg/session_ss/data.dart @@ -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 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?, + ); +} diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 0c1b9c45..04898c84 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -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/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/radio_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/models/space/data.dart'; import 'package:PiliPlus/pages/member/controller.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:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class MemberPage extends StatefulWidget { @@ -137,7 +135,7 @@ class _MemberPageState extends State { horizontal: 20, vertical: 16, ), - content: ReportPanel( + content: MemberReportPanel( name: _userController.username, mid: _mid, ), @@ -290,124 +288,3 @@ class _MemberPageState extends State { }; } } - -class ReportPanel extends StatefulWidget { - const ReportPanel({ - super.key, - required this.name, - required this.mid, - }); - - final dynamic name; - final dynamic mid; - - @override - State createState() => _ReportPanelState(); -} - -class _ReportPanelState extends State { - final List _reasonList = List.generate(3, (_) => false).toList(); - final Set _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( - 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), - ], - ), - ); -} diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index cd907fc4..d81faedc 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -343,11 +343,11 @@ class UserInfoCard extends StatelessWidget { if (GStorage.userInfo.get('userInfoCache') != null) { Get.toNamed( '/whisperDetail', - parameters: { - 'talkerId': card.mid ?? '', - 'name': card.name ?? '', - 'face': card.face ?? '', - 'mid': card.mid ?? '', + arguments: { + 'talkerId': int.parse(card.mid!), + 'name': card.name, + 'face': card.face, + 'mid': card.mid, }, ); } diff --git a/lib/pages/msg_feed_top/at_me/view.dart b/lib/pages/msg_feed_top/at_me/view.dart index 138a864d..deb5e544 100644 --- a/lib/pages/msg_feed_top/at_me/view.dart +++ b/lib/pages/msg_feed_top/at_me/view.dart @@ -39,9 +39,13 @@ class _AtMePageState extends State { 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( diff --git a/lib/pages/msg_feed_top/like_me/controller.dart b/lib/pages/msg_feed_top/like_me/controller.dart index 00cda0cc..b63ff8e7 100644 --- a/lib/pages/msg_feed_top/like_me/controller.dart +++ b/lib/pages/msg_feed_top/like_me/controller.dart @@ -18,6 +18,14 @@ class LikeMeController extends CommonDataController { queryData(); } + @override + Future queryData([bool isRefresh = true]) { + if (!isRefresh && isEnd) { + return Future.value(); + } + return super.queryData(isRefresh); + } + @override bool customHandleResponse(bool isRefresh, Success response) { MsgFeedLikeMe data = response.response; diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index 9a61e3ff..2d6b859f 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -28,6 +28,7 @@ class _LikeMePageState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('收到的赞'), @@ -39,9 +40,13 @@ class _LikeMePageState extends State { 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( @@ -52,8 +57,8 @@ class _LikeMePageState extends State { SliverPadding( padding: EdgeInsets.only( bottom: MediaQuery.paddingOf(context).bottom + 80), - sliver: - Obx(() => _buildBody(_likeMeController.loadingState.value)), + sliver: Obx(() => + _buildBody(theme, _likeMeController.loadingState.value)), ), ], ), @@ -61,7 +66,7 @@ class _LikeMePageState extends State { ); } - Widget _buildBody(LoadingState loadingState) { + Widget _buildBody(ThemeData theme, LoadingState loadingState) { return switch (loadingState) { Loading() => SliverList.builder( itemCount: 12, @@ -70,7 +75,6 @@ class _LikeMePageState extends State { }, ), Success(:var response) => () { - final theme = Theme.of(context); Pair, List> pair = response; List latest = pair.first; List total = pair.second; diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart index f2f40fd6..822f5df1 100644 --- a/lib/pages/msg_feed_top/reply_me/view.dart +++ b/lib/pages/msg_feed_top/reply_me/view.dart @@ -39,9 +39,13 @@ class _ReplyMePageState extends State { 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( diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index 2b680cf6..2cac83d0 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -576,21 +576,26 @@ class VideoIntroController extends GetxController { SmartDialog.showToast('账号未登录'); return; } + int? mid = videoDetail.value.owner?.mid; + if (mid == null) { + return; + } int attr = followStatus['attribute'] ?? 0; if (attr == 128) { dynamic res = await VideoHttp.relationMod( - mid: videoDetail.value.owner?.mid ?? -1, + mid: mid, act: 6, reSrc: 11, ); if (res['status']) { + GStorage.removeBlackMid(mid); followStatus['attribute'] = 0; } return; } else { RequestUtils.actionRelationMod( context: context, - mid: videoDetail.value.owner?.mid, + mid: mid, isFollow: attr != 0, followStatus: followStatus, callback: (attribute) { diff --git a/lib/pages/whisper/widgets/item.dart b/lib/pages/whisper/widgets/item.dart index fbb27b9a..10e5c48c 100644 --- a/lib/pages/whisper/widgets/item.dart +++ b/lib/pages/whisper/widgets/item.dart @@ -93,13 +93,13 @@ class WhisperSessionItem extends StatelessWidget { if (item.id.privateId.hasTalkerUid()) { Get.toNamed( '/whisperDetail', - parameters: { - 'talkerId': item.id.privateId.talkerUid.toString(), + arguments: { + 'talkerId': item.id.privateId.talkerUid.toInt(), 'name': item.sessionInfo.sessionName, 'face': item.sessionInfo.avatar.fallbackLayers.layers.first .resource.resImage.imageSrc.remote.url, if (item.sessionInfo.avatar.hasMid()) - 'mid': item.sessionInfo.avatar.mid.toString(), + 'mid': item.sessionInfo.avatar.mid.toInt(), }, ); return; diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 5cfdae91..538da5b9 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -19,10 +19,10 @@ import 'package:get/get.dart'; class WhisperDetailController extends CommonListController { late final ownerMid = Accounts.main.mid; - late int talkerId; - late String name; - late String face; - int? mid; + final int talkerId = Get.arguments['talkerId']; + final String name = Get.arguments['name']; + final String face = Get.arguments['face']; + final int? mid = Get.arguments['mid']; Int64? msgSeqno; @@ -32,14 +32,6 @@ class WhisperDetailController extends CommonListController { @override void 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(); } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 1e868726..190281ab 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -12,6 +12,7 @@ import 'package:PiliPlus/pages/common/common_publish_page.dart'; import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/pages/whisper_detail/controller.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/feed_back.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( top: false, diff --git a/lib/pages/whisper_link_setting/controller.dart b/lib/pages/whisper_link_setting/controller.dart new file mode 100644 index 00000000..43fde366 --- /dev/null +++ b/lib/pages/whisper_link_setting/controller.dart @@ -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?>> userState = + LoadingState?>.loading().obs; + final Rx> sessionSs = + LoadingState.loading().obs; + final Rx?>> msgDnd = + LoadingState?>.loading().obs; + + Future getUserInfo() async { + userState.value = await MsgHttp.imUserInfos(uids: [talkerUid]); + } + + Future getSessionSs() async { + sessionSs.value = await MsgHttp.getSessionSs(talkerUid: talkerUid); + } + + Future getMsgDnd() async { + msgDnd.value = await MsgHttp.getMsgDnd(uidsStr: talkerUid); + } + + Future 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 _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 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 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 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, + ), + ), + ); + } +} diff --git a/lib/pages/whisper_link_setting/view.dart b/lib/pages/whisper_link_setting/view.dart new file mode 100644 index 00000000..72fba19e --- /dev/null +++ b/lib/pages/whisper_link_setting/view.dart @@ -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 createState() => _WhisperLinkSettingPageState(); +} + +class _WhisperLinkSettingPageState extends State { + 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((Set 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?> 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 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( + (Set 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( + (Set 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?> 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( + (Set 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, + ), + ), + ); + } +} diff --git a/lib/pages/whisper_settings/widgets/item.dart b/lib/pages/whisper_settings/widgets/item.dart index f8f29559..f35c2115 100644 --- a/lib/pages/whisper_settings/widgets/item.dart +++ b/lib/pages/whisper_settings/widgets/item.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' show SelectItem, Setting, SettingSwitch; import 'package:flutter/material.dart'; @@ -120,43 +122,55 @@ class ImSettingsItem extends StatelessWidget { if (item.hasSelect()) { String? selected; + late final divider = Divider( + height: 1, + indent: 16, + color: outline.withOpacity(0.1), + ); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: item.select.item.map((e) { - if (e.selected) { - selected ??= e.text; - } - return ListTile( - dense: true, - onTap: () async { - if (!e.selected) { - for (var i in item.select.item) { - i.selected = false; - } - e.selected = true; - rebuild(); - - if (await onSet()) { - selected = e.text; - } else { + children: List.generate( + max(0, item.select.item.length * 2 - 1), + (index) { + if (index.isOdd) { + return divider; + } + final e = item.select.item[index ~/ 2]; + if (e.selected) { + selected ??= e.text; + } + return ListTile( + dense: true, + onTap: () async { + if (!e.selected) { for (var i in item.select.item) { - i.selected = i.text == selected; + i.selected = false; } + e.selected = true; 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), - trailing: e.selected - ? Icon( - size: 20, - Icons.check, - color: theme.colorScheme.primary, - ) - : null, - ); - }).toList(), + }, + title: Text(e.text, style: titleStyle), + trailing: e.selected + ? Icon( + size: 20, + Icons.check, + color: theme.colorScheme.primary, + ) + : null, + ); + }, + ), ); } diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index 0fdb7515..08aca2c6 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -172,8 +172,8 @@ extension ThreeDotItemTypeExt on ThreeDotItemType { case ThreeDotItemType.THREE_DOT_ITEM_TYPE_UP_HELPER: Get.toNamed( '/whisperDetail', - parameters: { - 'talkerId': '844424930131966', + arguments: { + 'talkerId': 844424930131966, 'name': 'UP主小助手', 'face': 'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png', diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 37c64750..46c4fada 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -496,6 +496,10 @@ class GStorage { 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 .values[setting.get(SettingBoxKey.memberTab, defaultValue: 0)];