feat: im settings

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-09 21:54:23 +08:00
parent a282baf5a2
commit 0f41d5b2f8
14 changed files with 865 additions and 9 deletions

View File

@@ -53,6 +53,11 @@ class GrpcUrl {
static const pinSession = '$im2/PinSession'; static const pinSession = '$im2/PinSession';
static const unpinSession = '$im2/UnpinSession'; static const unpinSession = '$im2/UnpinSession';
static const deleteSessionList = '$im2/DeleteSessionList'; 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 { class GrpcRepo {

View File

@@ -143,4 +143,60 @@ class ImGrpc {
DeleteSessionListReply.fromBuffer, DeleteSessionListReply.fromBuffer,
); );
} }
static Future<LoadingState<GetImSettingsReply>> 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<int, Setting>? settings}) {
return GrpcRepo.request(
GrpcUrl.setImSettings,
SetImSettingsReq(
settings: settings,
),
SetImSettingsReply.fromBuffer,
);
}
static Future<LoadingState<KeywordBlockingListReply>>
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,
);
}
} }

View File

@@ -8,7 +8,9 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class ContactPage extends StatefulWidget { class ContactPage extends StatefulWidget {
const ContactPage({super.key}); const ContactPage({super.key, this.isFromSelct = true});
final bool isFromSelct;
@override @override
State<ContactPage> createState() => _ContactPageState(); State<ContactPage> createState() => _ContactPageState();
@@ -45,7 +47,10 @@ class _ContactPageState extends State<ContactPage>
IconButton( IconButton(
onPressed: () async { onPressed: () async {
UserModel? userModel = await Get.dialog( UserModel? userModel = await Get.dialog(
FollowSearchPage(mid: mid), FollowSearchPage(
mid: mid,
isFromSelct: widget.isFromSelct,
),
useSafeArea: false, useSafeArea: false,
transitionDuration: const Duration(milliseconds: 120), transitionDuration: const Duration(milliseconds: 120),
); );
@@ -61,8 +66,14 @@ class _ContactPageState extends State<ContactPage>
body: tabBarView( body: tabBarView(
controller: _controller, controller: _controller,
children: [ children: [
FollowChildPage(mid: mid, onSelect: onSelect), FollowChildPage(
FansPage(mid: mid, onSelect: onSelect), mid: mid,
onSelect: widget.isFromSelct ? onSelect : null,
),
FansPage(
mid: mid,
onSelect: widget.isFromSelct ? onSelect : null,
),
], ],
), ),
); );

View File

@@ -49,7 +49,7 @@ class _FansPageState extends State<FansPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: widget.onSelect != null appBar: widget.mid != null
? null ? null
: AppBar(title: Text(isOwner ? '我的粉丝' : '$name的粉丝')), : AppBar(title: Text(isOwner ? '我的粉丝' : '$name的粉丝')),
body: SafeArea( body: SafeArea(

View File

@@ -7,9 +7,14 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class FollowSearchPage extends CommonSearchPage { class FollowSearchPage extends CommonSearchPage {
const FollowSearchPage({super.key, this.mid}); const FollowSearchPage({
super.key,
this.mid,
this.isFromSelct,
});
final int? mid; final int? mid;
final bool? isFromSelct;
@override @override
State<FollowSearchPage> createState() => _FollowSearchPageState(); State<FollowSearchPage> createState() => _FollowSearchPageState();
@@ -37,7 +42,7 @@ class _FollowSearchPageState extends CommonSearchPageState<FollowSearchPage,
} }
return FollowItem( return FollowItem(
item: list[index], item: list[index],
onSelect: widget.mid != null onSelect: widget.mid != null && widget.isFromSelct != false
? (userModel) { ? (userModel) {
Get.back(result: userModel); Get.back(result: userModel);
} }

View File

@@ -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/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.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/http/loading_state.dart';
import 'package:PiliPlus/models/msg/msgfeed_at_me.dart'; import 'package:PiliPlus/models/msg/msgfeed_at_me.dart';
import 'package:PiliPlus/pages/msg_feed_top/at_me/controller.dart'; import 'package:PiliPlus/pages/msg_feed_top/at_me/controller.dart';
import 'package:PiliPlus/pages/whisper_settings/view.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -27,6 +30,18 @@ class _AtMePageState extends State<AtMePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('@我的'), 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( body: refreshIndicator(
onRefresh: _atMeController.onRefresh, onRefresh: _atMeController.onRefresh,

View File

@@ -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/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.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/http/loading_state.dart';
import 'package:PiliPlus/models/msg/msgfeed_like_me.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/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/app_scheme.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -27,6 +30,18 @@ class _LikeMePageState extends State<LikeMePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('收到的赞'), 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( body: refreshIndicator(
onRefresh: _likeMeController.onRefresh, onRefresh: _likeMeController.onRefresh,

View File

@@ -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/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.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/http/loading_state.dart';
import 'package:PiliPlus/models/msg/msgfeed_reply_me.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/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/app_scheme.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -25,7 +28,21 @@ class _ReplyMePageState extends State<ReplyMePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( 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( body: refreshIndicator(
onRefresh: _replyMeController.onRefresh, onRefresh: _replyMeController.onRefresh,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -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/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart';
import 'package:PiliPlus/http/loading_state.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/controller.dart';
import 'package:PiliPlus/pages/whisper/widgets/item.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/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -41,7 +43,64 @@ class _WhisperPageState extends State<WhisperPage> {
Icons.cleaning_services, 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( body: refreshIndicator(

View File

@@ -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<KeywordBlockingItem>? getDataList(KeywordBlockingListReply response) {
count.value = response.items.length;
listLimit = response.listLimit;
charLimit = response.charLimit;
return response.items;
}
@override
Future<LoadingState<KeywordBlockingListReply>> customGetData() =>
ImGrpc.keywordBlockingList();
Future<void> 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<void> 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']);
}
}
}

View File

@@ -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<WhisperBlockPage> createState() => _WhisperBlockPageState();
}
class _WhisperBlockPageState extends State<WhisperBlockPage> {
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<List<KeywordBlockingItem>?> 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('添加消息屏蔽词')],
),
),
],
),
);
},
);
}
}

View File

@@ -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<GetImSettingsReply, PbMap<int, Setting>> {
WhisperSettingsController({
required this.imSettingType,
});
final IMSettingType imSettingType;
RxString title = ''.obs;
@override
void onInit() {
super.onInit();
queryData();
}
@override
bool customHandleResponse(
bool isRefresh, Success<GetImSettingsReply> response) {
title.value = response.response.pageTitle;
loadingState.value = LoadingState.success(response.response.settings);
return true;
}
@override
Future<LoadingState<GetImSettingsReply>> customGetData() =>
ImGrpc.getImSettings(type: imSettingType);
Future<bool> onSet(PbMap<int, Setting> settings) async {
var res = await ImGrpc.setImSettings(settings: settings);
if (!res['status']) {
SmartDialog.showToast('err: ${res['msg']}');
}
return res['status'];
}
}

View File

@@ -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<PbMap<int, Setting>>? onUpdate;
@override
State<WhisperSettingsPage> createState() => _WhisperSettingsPageState();
}
class _WhisperSettingsPageState extends State<WhisperSettingsPage> {
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<PbMap<int, Setting>> loadingState) {
return switch (loadingState) {
Loading() => const SizedBox.shrink(),
Success<PbMap<int, Setting>>() => 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<int, Setting> settings = PbMap<int, Setting>(
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<int, Setting> settings =
PbMap<int, Setting>(
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,
),
};
}
}

View File

@@ -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<bool> 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<void> 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<Icon?>((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();
}
}