From 0f41d5b2f8a308a972b4d382c69ded28f77dc086 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Fri, 9 May 2025 21:54:23 +0800 Subject: [PATCH] feat: im settings Signed-off-by: bggRGjQaUbCoE --- lib/grpc/grpc_repo.dart | 5 + lib/grpc/im.dart | 56 +++++ lib/pages/contact/view.dart | 19 +- lib/pages/fan/view.dart | 2 +- lib/pages/follow_search/view.dart | 9 +- lib/pages/msg_feed_top/at_me/view.dart | 15 ++ lib/pages/msg_feed_top/like_me/view.dart | 15 ++ lib/pages/msg_feed_top/reply_me/view.dart | 19 +- lib/pages/whisper/view.dart | 61 +++++- lib/pages/whisper_block/controller.dart | 58 +++++ lib/pages/whisper_block/view.dart | 219 +++++++++++++++++++ lib/pages/whisper_settings/controller.dart | 45 ++++ lib/pages/whisper_settings/view.dart | 186 ++++++++++++++++ lib/pages/whisper_settings/widgets/item.dart | 165 ++++++++++++++ 14 files changed, 865 insertions(+), 9 deletions(-) create mode 100644 lib/pages/whisper_block/controller.dart create mode 100644 lib/pages/whisper_block/view.dart create mode 100644 lib/pages/whisper_settings/controller.dart create mode 100644 lib/pages/whisper_settings/view.dart create mode 100644 lib/pages/whisper_settings/widgets/item.dart diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index c884f0e9..5ac801d1 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -53,6 +53,11 @@ class GrpcUrl { static const pinSession = '$im2/PinSession'; static const unpinSession = '$im2/UnpinSession'; static const deleteSessionList = '$im2/DeleteSessionList'; + static const getImSettings = '$im2/GetImSettings'; + static const setImSettings = '$im2/SetImSettings'; + static const keywordBlockingList = '$im2/KeywordBlockingList'; + static const keywordBlockingAdd = '$im2/KeywordBlockingAdd'; + static const keywordBlockingDelete = '$im2/KeywordBlockingDelete'; } class GrpcRepo { diff --git a/lib/grpc/im.dart b/lib/grpc/im.dart index d6163981..021b18e8 100644 --- a/lib/grpc/im.dart +++ b/lib/grpc/im.dart @@ -143,4 +143,60 @@ class ImGrpc { DeleteSessionListReply.fromBuffer, ); } + + static Future> getImSettings( + {IMSettingType? type}) async { + var res = await GrpcRepo.request( + GrpcUrl.getImSettings, + GetImSettingsReq( + type: type, + ), + GetImSettingsReply.fromBuffer, + ); + if (res['status']) { + return LoadingState.success(res['data']); + } else { + return LoadingState.error(res['msg']); + } + } + + static Future setImSettings({PbMap? settings}) { + return GrpcRepo.request( + GrpcUrl.setImSettings, + SetImSettingsReq( + settings: settings, + ), + SetImSettingsReply.fromBuffer, + ); + } + + static Future> + keywordBlockingList() async { + var res = await GrpcRepo.request( + GrpcUrl.keywordBlockingList, + KeywordBlockingListReq(), + KeywordBlockingListReply.fromBuffer, + ); + if (res['status']) { + return LoadingState.success(res['data']); + } else { + return LoadingState.error(res['msg']); + } + } + + static Future keywordBlockingAdd(String keyword) { + return GrpcRepo.request( + GrpcUrl.keywordBlockingAdd, + KeywordBlockingAddReq(keyword: keyword), + KeywordBlockingAddReply.fromBuffer, + ); + } + + static Future keywordBlockingDelete(String keyword) { + return GrpcRepo.request( + GrpcUrl.keywordBlockingDelete, + KeywordBlockingDeleteReq(keyword: keyword), + KeywordBlockingDeleteReply.fromBuffer, + ); + } } diff --git a/lib/pages/contact/view.dart b/lib/pages/contact/view.dart index 68533ac5..7fe0072e 100644 --- a/lib/pages/contact/view.dart +++ b/lib/pages/contact/view.dart @@ -8,7 +8,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ContactPage extends StatefulWidget { - const ContactPage({super.key}); + const ContactPage({super.key, this.isFromSelct = true}); + + final bool isFromSelct; @override State createState() => _ContactPageState(); @@ -45,7 +47,10 @@ class _ContactPageState extends State IconButton( onPressed: () async { UserModel? userModel = await Get.dialog( - FollowSearchPage(mid: mid), + FollowSearchPage( + mid: mid, + isFromSelct: widget.isFromSelct, + ), useSafeArea: false, transitionDuration: const Duration(milliseconds: 120), ); @@ -61,8 +66,14 @@ class _ContactPageState extends State body: tabBarView( controller: _controller, children: [ - FollowChildPage(mid: mid, onSelect: onSelect), - FansPage(mid: mid, onSelect: onSelect), + FollowChildPage( + mid: mid, + onSelect: widget.isFromSelct ? onSelect : null, + ), + FansPage( + mid: mid, + onSelect: widget.isFromSelct ? onSelect : null, + ), ], ), ); diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart index cca7452b..d3375e19 100644 --- a/lib/pages/fan/view.dart +++ b/lib/pages/fan/view.dart @@ -49,7 +49,7 @@ class _FansPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: widget.onSelect != null + appBar: widget.mid != null ? null : AppBar(title: Text(isOwner ? '我的粉丝' : '$name的粉丝')), body: SafeArea( diff --git a/lib/pages/follow_search/view.dart b/lib/pages/follow_search/view.dart index c8827cb4..49ab8ddd 100644 --- a/lib/pages/follow_search/view.dart +++ b/lib/pages/follow_search/view.dart @@ -7,9 +7,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class FollowSearchPage extends CommonSearchPage { - const FollowSearchPage({super.key, this.mid}); + const FollowSearchPage({ + super.key, + this.mid, + this.isFromSelct, + }); final int? mid; + final bool? isFromSelct; @override State createState() => _FollowSearchPageState(); @@ -37,7 +42,7 @@ class _FollowSearchPageState extends CommonSearchPageState { return Scaffold( appBar: AppBar( title: const Text('@我的'), + actions: [ + IconButton( + onPressed: () { + Get.to( + const WhisperSettingsPage( + imSettingType: IMSettingType.SETTING_TYPE_OLD_AT_ME), + ); + }, + icon: const Icon(size: 22, Icons.settings), + ), + const SizedBox(width: 16), + ], ), body: refreshIndicator( onRefresh: _atMeController.onRefresh, diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index a9e713af..5dd0c987 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -4,9 +4,12 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pbenum.dart' + show IMSettingType; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/msg/msgfeed_like_me.dart'; import 'package:PiliPlus/pages/msg_feed_top/like_me/controller.dart'; +import 'package:PiliPlus/pages/whisper_settings/view.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -27,6 +30,18 @@ class _LikeMePageState extends State { return Scaffold( appBar: AppBar( title: const Text('收到的赞'), + actions: [ + IconButton( + onPressed: () { + Get.to( + const WhisperSettingsPage( + imSettingType: IMSettingType.SETTING_TYPE_OLD_RECEIVE_LIKE), + ); + }, + icon: const Icon(size: 22, Icons.settings), + ), + const SizedBox(width: 16), + ], ), body: refreshIndicator( onRefresh: _likeMeController.onRefresh, diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart index e92aacbe..ff0cd638 100644 --- a/lib/pages/msg_feed_top/reply_me/view.dart +++ b/lib/pages/msg_feed_top/reply_me/view.dart @@ -3,9 +3,12 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pbenum.dart' + show IMSettingType; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/msg/msgfeed_reply_me.dart'; import 'package:PiliPlus/pages/msg_feed_top/reply_me/controller.dart'; +import 'package:PiliPlus/pages/whisper_settings/view.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -25,7 +28,21 @@ class _ReplyMePageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( - appBar: AppBar(title: const Text('回复我的')), + appBar: AppBar( + title: const Text('回复我的'), + actions: [ + IconButton( + onPressed: () { + Get.to( + const WhisperSettingsPage( + imSettingType: IMSettingType.SETTING_TYPE_OLD_REPLY_ME), + ); + }, + icon: const Icon(size: 22, Icons.settings), + ), + const SizedBox(width: 16), + ], + ), body: refreshIndicator( onRefresh: _replyMeController.onRefresh, child: CustomScrollView( diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 0827e965..c421f4f6 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -4,8 +4,10 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/contact/view.dart'; import 'package:PiliPlus/pages/whisper/controller.dart'; import 'package:PiliPlus/pages/whisper/widgets/item.dart'; +import 'package:PiliPlus/pages/whisper_settings/view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -41,7 +43,64 @@ class _WhisperPageState extends State { Icons.cleaning_services, ), ), - const SizedBox(width: 16), + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: () { + Get.to(const WhisperSettingsPage( + imSettingType: IMSettingType.SETTING_TYPE_NEED_ALL, + )); + }, + child: const Row( + children: [ + Icon(size: 19, Icons.settings), + Text(' 消息设置'), + ], + ), + ), + PopupMenuItem( + onTap: () { + Get.toNamed( + '/whisperDetail', + parameters: { + 'talkerId': '844424930131966', + 'name': 'UP主小助手', + 'face': + 'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png', + }, + ); + }, + child: const Row( + children: [ + Icon(size: 18, Icons.live_tv), + Text(' UP主小助手'), + ], + ), + ), + // PopupMenuItem( + // onTap: () {}, + // child: const Row( + // children: [ + // Icon(size: 19, Icons.notifications_none), + // Text(' 应援团消息助手'), + // ], + // ), + // ), + PopupMenuItem( + onTap: () { + Get.to(const ContactPage(isFromSelct: false)); + }, + child: const Row( + children: [ + Icon(size: 19, Icons.account_box_outlined), + Text(' 通讯录'), + ], + ), + ), + ]; + }, + ), ], ), body: refreshIndicator( diff --git a/lib/pages/whisper_block/controller.dart b/lib/pages/whisper_block/controller.dart new file mode 100644 index 00000000..9a82554a --- /dev/null +++ b/lib/pages/whisper_block/controller.dart @@ -0,0 +1,58 @@ +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/pages/common/common_list_controller.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class WhisperBlockController extends CommonListController< + KeywordBlockingListReply, KeywordBlockingItem> { + @override + void onInit() { + super.onInit(); + queryData(); + } + + RxInt count = 0.obs; + int? listLimit; + int? charLimit; + + @override + List? getDataList(KeywordBlockingListReply response) { + count.value = response.items.length; + listLimit = response.listLimit; + charLimit = response.charLimit; + return response.items; + } + + @override + Future> customGetData() => + ImGrpc.keywordBlockingList(); + + Future onAdd(String keyword) async { + var res = await ImGrpc.keywordBlockingAdd(keyword); + if (res['status']) { + Get.back(); + loadingState + ..value.data!.add(KeywordBlockingItem(keyword: keyword)) + ..refresh(); + count.value += 1; + SmartDialog.showToast('添加成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } + + Future onRemove(KeywordBlockingItem item) async { + var res = await ImGrpc.keywordBlockingDelete(item.keyword); + if (res['status']) { + loadingState + ..value.data!.remove(item) + ..refresh(); + count.value -= 1; + SmartDialog.showToast('删除成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } +} diff --git a/lib/pages/whisper_block/view.dart b/lib/pages/whisper_block/view.dart new file mode 100644 index 00000000..eaf72203 --- /dev/null +++ b/lib/pages/whisper_block/view.dart @@ -0,0 +1,219 @@ +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' + show KeywordBlockingItem; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/whisper_block/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; + +class WhisperBlockPage extends StatefulWidget { + const WhisperBlockPage({ + super.key, + }); + + @override + State createState() => _WhisperBlockPageState(); +} + +class _WhisperBlockPageState extends State { + final _controller = Get.put(WhisperBlockController()); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar(title: const Text('消息屏蔽词')), + body: Obx(() => _buildBody(theme, _controller.loadingState.value)), + ); + } + + Widget _buildBody( + ThemeData theme, LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => loadingState.response?.isNotEmpty == true + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '点击屏蔽词即可删除', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, + ), + ), + if (_controller.listLimit != null) + Obx( + () => Text( + '${_controller.count.value}/${_controller.listLimit}', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Wrap( + spacing: 12, + runSpacing: 12, + children: loadingState.response! + .map((e) => SearchText( + text: e.keyword, + onTap: (keyword) { + showConfirmDialog( + context: context, + title: '删除屏蔽词?', + content: '该屏蔽词将不再生效', + onConfirm: () { + _controller.onRemove(e); + }, + ); + }, + )) + .toList(), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only( + left: 25, + right: 25, + bottom: MediaQuery.paddingOf(context).bottom + 10, + ), + child: FilledButton.tonal( + onPressed: _onAdd, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.add, size: 22), Text('添加消息屏蔽词')], + ), + ), + ), + ], + ) + : SizedBox.expand( + child: Column( + children: [ + const Spacer(), + SvgPicture.asset("assets/images/error.svg", height: 156), + const SizedBox(height: 6), + const Text( + '还未添加屏蔽词', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 6), + const Text('添加后,将不再接受包含屏蔽词的消息'), + const SizedBox(height: 6), + FilledButton.tonal( + onPressed: _onAdd, + style: FilledButton.styleFrom( + visualDensity: VisualDensity.compact), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, size: 22), + Text('添加'), + ], + ), + ), + const Spacer(flex: 2), + ], + ), + ), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } + + void _onAdd() { + String keyword = ''; + showModalBottomSheet( + context: context, + enableDrag: false, + useSafeArea: true, + isScrollControlled: true, + builder: (context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12) + + EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + + MediaQuery.viewInsetsOf(context).bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '添加消息屏蔽词', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: Get.back, + child: Icon( + Icons.clear, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 12), + TextFormField( + autofocus: true, + maxLength: _controller.charLimit, + decoration: InputDecoration( + isDense: true, + hintText: '请输入', + hintStyle: const TextStyle(fontSize: 14), + contentPadding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(25)), + ), + filled: true, + fillColor: theme.colorScheme.onInverseSurface, + ), + onChanged: (value) => keyword = value, + ), + const SizedBox(height: 12), + FilledButton.tonal( + onPressed: () { + if (keyword.isNotEmpty) { + _controller.onAdd(keyword); + } + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [Icon(Icons.add, size: 22), Text('添加消息屏蔽词')], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/whisper_settings/controller.dart b/lib/pages/whisper_settings/controller.dart new file mode 100644 index 00000000..3a66f15a --- /dev/null +++ b/lib/pages/whisper_settings/controller.dart @@ -0,0 +1,45 @@ +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' + show GetImSettingsReply, IMSettingType, Setting; +import 'package:PiliPlus/grpc/im.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_data_controller.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:protobuf/protobuf.dart' show PbMap; + +class WhisperSettingsController + extends CommonDataController> { + WhisperSettingsController({ + required this.imSettingType, + }); + + final IMSettingType imSettingType; + + RxString title = ''.obs; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + bool customHandleResponse( + bool isRefresh, Success response) { + title.value = response.response.pageTitle; + loadingState.value = LoadingState.success(response.response.settings); + return true; + } + + @override + Future> customGetData() => + ImGrpc.getImSettings(type: imSettingType); + + Future onSet(PbMap settings) async { + var res = await ImGrpc.setImSettings(settings: settings); + if (!res['status']) { + SmartDialog.showToast('err: ${res['msg']}'); + } + return res['status']; + } +} diff --git a/lib/pages/whisper_settings/view.dart b/lib/pages/whisper_settings/view.dart new file mode 100644 index 00000000..a0561fd9 --- /dev/null +++ b/lib/pages/whisper_settings/view.dart @@ -0,0 +1,186 @@ +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' + show IMSettingType, Setting; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/whisper_block/view.dart'; +import 'package:PiliPlus/pages/whisper_settings/controller.dart'; +import 'package:PiliPlus/pages/whisper_settings/widgets/item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:protobuf/protobuf.dart' show PbMap; + +class WhisperSettingsPage extends StatefulWidget { + const WhisperSettingsPage({ + super.key, + required this.imSettingType, + this.onUpdate, + }); + + final IMSettingType imSettingType; + final ValueChanged>? onUpdate; + + @override + State createState() => _WhisperSettingsPageState(); +} + +class _WhisperSettingsPageState extends State { + late final WhisperSettingsController _controller = Get.put( + WhisperSettingsController(imSettingType: widget.imSettingType), + tag: widget.imSettingType.name, + ); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Obx(() => Text(_controller.title.value)), + ), + body: Obx(() => _buildBody(theme, _controller.loadingState.value)), + ); + } + + Widget _buildBody( + ThemeData theme, LoadingState> loadingState) { + return switch (loadingState) { + Loading() => const SizedBox.shrink(), + Success>() => Builder(builder: (context) { + final keys = loadingState.response.keys.toList()..sort(); + return ListView.separated( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + itemCount: keys.length, + itemBuilder: (context, index) { + final key = keys[index]; + final item = loadingState.response[key]!; + return ImSettingsItem( + item: item, + onSet: () async { + PbMap settings = PbMap( + loadingState.response.keyFieldType, + loadingState.response.valueFieldType, + )..[key] = item; + final res = await _controller.onSet(settings); + if (res) { + widget.onUpdate?.call(settings); + } + return res; + }, + onRedirect: () { + if (item.redirect.settingPage.hasParentSettingType()) { + Get.to( + WhisperSettingsPage( + imSettingType: + item.redirect.settingPage.parentSettingType, + onUpdate: (value) { + _controller.loadingState + ..value + .data[key] + ?.redirect + .settingPage + .subSettings + .addAll(value) + ..refresh(); + }, + ), + preventDuplicates: false, + ); + } else if (item.redirect.hasWindowSelect()) { + String? selected; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: + const EdgeInsets.symmetric(vertical: 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: item.redirect.windowSelect.item.map( + (e) { + if (e.selected) { + selected ??= e.text; + } + return ListTile( + dense: true, + onTap: () async { + if (!e.selected) { + Get.back(); + for (var j + in item.redirect.windowSelect.item) { + j.selected = false; + } + item.redirect.selectedSummary = e.text; + e.selected = true; + _controller.loadingState.refresh(); + PbMap settings = + PbMap( + loadingState.response.keyFieldType, + loadingState.response.valueFieldType, + )..[key] = item; + final res = + await _controller.onSet(settings); + if (!res) { + for (var j in item + .redirect.windowSelect.item) { + j.selected = j.text == selected; + } + item.redirect.selectedSummary = + selected!; + _controller.loadingState.refresh(); + } + } + }, + title: Text( + e.text, + style: TextStyle( + fontSize: 14, + color: e.selected + ? theme.colorScheme.primary + : null, + ), + ), + ); + }, + ).toList(), + ), + ); + }, + ); + } else if (item.redirect.otherPage.hasUrl()) { + if (item.redirect.title == '黑名单') { + Get.toNamed('/blackListPage'); + } else if (item.redirect.otherPage.url.startsWith('http')) { + Get.toNamed('/webview', + parameters: {'url': item.redirect.otherPage.url}); + } else { + SmartDialog.showToast(item.redirect.otherPage.url); + } + } else if (item.redirect.settingPage.hasUrl()) { + if (item.redirect.title == '消息屏蔽词') { + Get.to(const WhisperBlockPage()); + } else if (item.redirect.settingPage.url + .startsWith('http')) { + Get.toNamed('/webview', + parameters: {'url': item.redirect.settingPage.url}); + } else { + SmartDialog.showToast(item.redirect.settingPage.url); + } + } + }, + ); + }, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.colorScheme.outline.withOpacity(0.1), + ), + ); + }), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } +} diff --git a/lib/pages/whisper_settings/widgets/item.dart b/lib/pages/whisper_settings/widgets/item.dart new file mode 100644 index 00000000..f8f29559 --- /dev/null +++ b/lib/pages/whisper_settings/widgets/item.dart @@ -0,0 +1,165 @@ +import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' + show SelectItem, Setting, SettingSwitch; +import 'package:flutter/material.dart'; + +class ImSettingsItem extends StatelessWidget { + const ImSettingsItem({ + super.key, + required this.item, + required this.onSet, + required this.onRedirect, + }); + + final Setting item; + final Future Function() onSet; + final VoidCallback onRedirect; + + @override + Widget build(BuildContext context) { + void rebuild() { + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + } + + const titleStyle = TextStyle(fontSize: 14); + final theme = Theme.of(context); + final outline = theme.colorScheme.outline; + final subtitleStyle = TextStyle(fontSize: 13, color: outline); + + if (item.hasSwitch_1()) { + Future onChanged() async { + item.switch_1.switchOn = !item.switch_1.switchOn; + rebuild(); + if (!await onSet()) { + item.switch_1.switchOn = !item.switch_1.switchOn; + rebuild(); + } + } + + return ListTile( + dense: true, + onTap: onChanged, + title: Text( + item.switch_1.title, + style: titleStyle, + ), + subtitle: item.switch_1.hasSubtitle() + ? Text(item.switch_1.subtitle, style: subtitleStyle) + : null, + trailing: Transform.scale( + alignment: Alignment.centerRight, + scale: 0.8, + child: Switch( + thumbIcon: WidgetStateProperty.resolveWith((states) { + if (states.isNotEmpty && states.first == WidgetState.selected) { + return const Icon(Icons.done); + } + return null; + }), + value: item.switch_1.switchOn, + onChanged: (value) => onChanged(), + ), + ), + ); + } + + if (item.hasRedirect()) { + SelectItem? selected; + SettingSwitch? sw1tch; + if (item.redirect.settingPage.subSettings.isNotEmpty) { + for (var subItem in item.redirect.settingPage.subSettings.values) { + if (subItem.hasSelect()) { + for (var i in subItem.select.item) { + if (i.selected) { + selected = i; + break; + } + } + } else if (subItem.hasSwitch_1()) { + if (subItem.switch_1.switchOn) { + sw1tch = subItem.switch_1; + break; + } + } + } + } + return ListTile( + dense: true, + onTap: onRedirect, + title: Text( + item.redirect.title, + style: titleStyle, + ), + subtitle: item.redirect.hasSubtitle() + ? Text(item.redirect.subtitle, style: subtitleStyle) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (selected != null) + Text( + selected.text, + style: TextStyle(fontSize: 13, color: outline), + ) + else if (sw1tch != null) + Text( + sw1tch.title, + style: TextStyle(fontSize: 13, color: outline), + ) + else if (item.redirect.hasSelectedSummary()) + Text( + item.redirect.selectedSummary, + style: TextStyle(fontSize: 13, color: outline), + ), + Icon(color: outline, Icons.keyboard_arrow_right), + ], + ), + ); + } + + if (item.hasSelect()) { + String? selected; + 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 { + 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(), + ); + } + + return const SizedBox.shrink(); + } +}