Support proxies search Support svg display Optimize config persistence Add some scenes auto close connections Update core Optimize more details
805 lines
25 KiB
Dart
805 lines
25 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/plugins/app.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
class AccessView extends ConsumerStatefulWidget {
|
|
const AccessView({super.key});
|
|
|
|
@override
|
|
ConsumerState<AccessView> createState() => _AccessViewState();
|
|
}
|
|
|
|
class _AccessViewState extends ConsumerState<AccessView> {
|
|
List<String> acceptList = [];
|
|
List<String> rejectList = [];
|
|
late ScrollController _controller;
|
|
final _completer = Completer();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_updateInitList();
|
|
_controller = ScrollController();
|
|
_completer.complete(globalState.appController.getPackages());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
_updateInitList() {
|
|
acceptList = globalState.config.vpnProps.accessControl.acceptList;
|
|
rejectList = globalState.config.vpnProps.accessControl.rejectList;
|
|
}
|
|
|
|
Widget _buildSearchButton() {
|
|
return IconButton(
|
|
tooltip: appLocalizations.search,
|
|
onPressed: () {
|
|
showSearch(
|
|
context: context,
|
|
delegate: AccessControlSearchDelegate(
|
|
acceptList: acceptList,
|
|
rejectList: rejectList,
|
|
),
|
|
).then(
|
|
(_) => setState(
|
|
() {
|
|
_updateInitList();
|
|
},
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.search),
|
|
);
|
|
}
|
|
|
|
Widget _buildSelectedAllButton({
|
|
required bool isSelectedAll,
|
|
required List<String> allValueList,
|
|
}) {
|
|
final tooltip = isSelectedAll
|
|
? appLocalizations.cancelSelectAll
|
|
: appLocalizations.selectAll;
|
|
return IconButton(
|
|
tooltip: tooltip,
|
|
onPressed: () {
|
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
|
final isAccept =
|
|
state.accessControl.mode == AccessControlMode.acceptSelected;
|
|
if (isSelectedAll) {
|
|
return switch (isAccept) {
|
|
true => state.copyWith.accessControl(
|
|
acceptList: [],
|
|
),
|
|
false => state.copyWith.accessControl(
|
|
rejectList: [],
|
|
),
|
|
};
|
|
} else {
|
|
return switch (isAccept) {
|
|
true => state.copyWith.accessControl(
|
|
acceptList: allValueList,
|
|
),
|
|
false => state.copyWith.accessControl(
|
|
rejectList: allValueList,
|
|
),
|
|
};
|
|
}
|
|
});
|
|
},
|
|
icon: isSelectedAll
|
|
? const Icon(Icons.deselect)
|
|
: const Icon(Icons.select_all),
|
|
);
|
|
}
|
|
|
|
_intelligentSelected() async {
|
|
final packageNames = ref.read(
|
|
packageListSelectorStateProvider.select(
|
|
(state) => state.list.map((item) => item.packageName),
|
|
),
|
|
);
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
if (commonScaffoldState?.mounted != true) return;
|
|
final selectedPackageNames =
|
|
(await commonScaffoldState?.loadingRun<List<String>>(
|
|
() async {
|
|
return await app?.getChinaPackageNames() ?? [];
|
|
},
|
|
))
|
|
?.toSet() ??
|
|
{};
|
|
final acceptList = packageNames
|
|
.where((item) => !selectedPackageNames.contains(item))
|
|
.toList();
|
|
final rejectList = packageNames
|
|
.where((item) => selectedPackageNames.contains(item))
|
|
.toList();
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
acceptList: acceptList,
|
|
rejectList: rejectList,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingButton() {
|
|
return IconButton(
|
|
onPressed: () async {
|
|
final res = await showSheet<int>(
|
|
context: context,
|
|
props: SheetProps(
|
|
isScrollControlled: true,
|
|
),
|
|
builder: (_, type) {
|
|
return AdaptiveSheetScaffold(
|
|
type: type,
|
|
body: AccessControlPanel(),
|
|
title: appLocalizations.proxiesSetting,
|
|
);
|
|
},
|
|
);
|
|
if (res == 1) {
|
|
_intelligentSelected();
|
|
}
|
|
},
|
|
icon: const Icon(Icons.tune),
|
|
);
|
|
}
|
|
|
|
_handleSelected(List<String> valueList, Package package, bool? value) {
|
|
if (value == true) {
|
|
valueList.add(package.packageName);
|
|
} else {
|
|
valueList.remove(package.packageName);
|
|
}
|
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
|
return switch (
|
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
|
true => state.copyWith.accessControl(
|
|
acceptList: valueList,
|
|
),
|
|
false => state.copyWith.accessControl(
|
|
rejectList: valueList,
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = ref.watch(packageListSelectorStateProvider);
|
|
final accessControl = state.accessControl;
|
|
final accessControlMode = accessControl.mode;
|
|
final packages = state.getSortList(
|
|
accessControlMode == AccessControlMode.acceptSelected
|
|
? acceptList
|
|
: rejectList,
|
|
);
|
|
final currentList = accessControl.currentList;
|
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
|
final valueList = currentList.intersection(packageNameList);
|
|
final describe = accessControlMode == AccessControlMode.acceptSelected
|
|
? appLocalizations.accessControlAllowDesc
|
|
: appLocalizations.accessControlNotAllowDesc;
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
Flexible(
|
|
flex: 0,
|
|
child: ListItem.switchItem(
|
|
title: Text(appLocalizations.appAccessControl),
|
|
delegate: SwitchDelegate(
|
|
value: accessControl.enable,
|
|
onChanged: (enable) {
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
enable: enable,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
child: Divider(
|
|
height: 12,
|
|
),
|
|
),
|
|
Flexible(
|
|
child: DisabledMask(
|
|
status: !accessControl.enable,
|
|
child: Column(
|
|
children: [
|
|
ActivateBox(
|
|
active: accessControl.enable,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 4,
|
|
bottom: 4,
|
|
left: 16,
|
|
right: 8,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
Expanded(
|
|
child: IntrinsicHeight(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
appLocalizations.selected,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelLarge
|
|
?.copyWith(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primary,
|
|
),
|
|
),
|
|
),
|
|
const Flexible(
|
|
child: SizedBox(
|
|
width: 8,
|
|
),
|
|
),
|
|
Flexible(
|
|
child: Text(
|
|
"${valueList.length}",
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelLarge
|
|
?.copyWith(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Flexible(
|
|
child: Text(describe),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Flexible(
|
|
child: _buildSearchButton(),
|
|
),
|
|
Flexible(
|
|
child: _buildSelectedAllButton(
|
|
isSelectedAll:
|
|
valueList.length == packageNameList.length,
|
|
allValueList: packageNameList,
|
|
),
|
|
),
|
|
Flexible(
|
|
child: _buildSettingButton(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 1,
|
|
child: FutureBuilder(
|
|
future: _completer.future,
|
|
builder: (_, snapshot) {
|
|
if (snapshot.connectionState != ConnectionState.done) {
|
|
return Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
return packages.isEmpty
|
|
? NullStatus(
|
|
label: appLocalizations.noData,
|
|
)
|
|
: CommonScrollBar(
|
|
controller: _controller,
|
|
child: ListView.builder(
|
|
controller: _controller,
|
|
itemCount: packages.length,
|
|
itemExtent: 72,
|
|
itemBuilder: (_, index) {
|
|
final package = packages[index];
|
|
return PackageListItem(
|
|
key: Key(package.packageName),
|
|
package: package,
|
|
value: valueList
|
|
.contains(package.packageName),
|
|
isActive: accessControl.enable,
|
|
onChanged: (value) {
|
|
_handleSelected(
|
|
valueList, package, value);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class PackageListItem extends StatelessWidget {
|
|
final Package package;
|
|
final bool value;
|
|
final bool isActive;
|
|
final void Function(bool?) onChanged;
|
|
|
|
const PackageListItem({
|
|
super.key,
|
|
required this.package,
|
|
required this.value,
|
|
required this.isActive,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FadeScaleEnterBox(
|
|
child: ActivateBox(
|
|
active: isActive,
|
|
child: ListItem.checkbox(
|
|
leading: SizedBox(
|
|
width: 48,
|
|
height: 48,
|
|
child: FutureBuilder<ImageProvider?>(
|
|
future: app?.getPackageIcon(package.packageName),
|
|
builder: (_, snapshot) {
|
|
if (!snapshot.hasData && snapshot.data == null) {
|
|
return Container();
|
|
} else {
|
|
return Image(
|
|
image: snapshot.data!,
|
|
gaplessPlayback: true,
|
|
width: 48,
|
|
height: 48,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
title: Text(
|
|
package.label,
|
|
style: const TextStyle(
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
maxLines: 1,
|
|
),
|
|
subtitle: Text(
|
|
package.packageName,
|
|
style: const TextStyle(
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
maxLines: 1,
|
|
),
|
|
delegate: CheckboxDelegate(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AccessControlSearchDelegate extends SearchDelegate {
|
|
List<String> acceptList = [];
|
|
List<String> rejectList = [];
|
|
|
|
AccessControlSearchDelegate({
|
|
required this.acceptList,
|
|
required this.rejectList,
|
|
});
|
|
|
|
@override
|
|
List<Widget>? buildActions(BuildContext context) {
|
|
return [
|
|
IconButton(
|
|
onPressed: () {
|
|
if (query.isEmpty) {
|
|
close(context, null);
|
|
return;
|
|
}
|
|
query = '';
|
|
},
|
|
icon: const Icon(Icons.clear),
|
|
),
|
|
const SizedBox(
|
|
width: 8,
|
|
)
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget? buildLeading(BuildContext context) {
|
|
return IconButton(
|
|
onPressed: () {
|
|
close(context, null);
|
|
},
|
|
icon: const Icon(Icons.arrow_back),
|
|
);
|
|
}
|
|
|
|
_handleSelected(
|
|
WidgetRef ref, List<String> valueList, Package package, bool? value) {
|
|
if (value == true) {
|
|
valueList.add(package.packageName);
|
|
} else {
|
|
valueList.remove(package.packageName);
|
|
}
|
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
|
return switch (
|
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
|
true => state.copyWith.accessControl(
|
|
acceptList: valueList,
|
|
),
|
|
false => state.copyWith.accessControl(
|
|
rejectList: valueList,
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
Widget _packageList() {
|
|
final lowQuery = query.toLowerCase();
|
|
return Consumer(
|
|
builder: (context, ref, __) {
|
|
final vm3 = ref.watch(
|
|
packageListSelectorStateProvider.select(
|
|
(state) => VM3(
|
|
a: state.getSortList(
|
|
state.accessControl.mode == AccessControlMode.acceptSelected
|
|
? acceptList
|
|
: rejectList,
|
|
),
|
|
b: state.accessControl.enable,
|
|
c: state.accessControl.currentList,
|
|
),
|
|
),
|
|
);
|
|
final packages = vm3.a;
|
|
final queryPackages = packages
|
|
.where(
|
|
(package) =>
|
|
package.label.toLowerCase().contains(lowQuery) ||
|
|
package.packageName.contains(lowQuery),
|
|
)
|
|
.toList();
|
|
final isAccessControl = vm3.b;
|
|
final currentList = vm3.c;
|
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
|
final valueList = currentList.intersection(packageNameList);
|
|
return DisabledMask(
|
|
status: !isAccessControl,
|
|
child: ListView.builder(
|
|
itemCount: queryPackages.length,
|
|
itemBuilder: (_, index) {
|
|
final package = queryPackages[index];
|
|
return PackageListItem(
|
|
key: Key(package.packageName),
|
|
package: package,
|
|
value: valueList.contains(package.packageName),
|
|
isActive: isAccessControl,
|
|
onChanged: (value) {
|
|
_handleSelected(
|
|
ref,
|
|
valueList,
|
|
package,
|
|
value,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget buildResults(BuildContext context) {
|
|
return buildSuggestions(context);
|
|
}
|
|
|
|
@override
|
|
Widget buildSuggestions(BuildContext context) {
|
|
return _packageList();
|
|
}
|
|
}
|
|
|
|
class AccessControlPanel extends ConsumerStatefulWidget {
|
|
const AccessControlPanel({
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
ConsumerState createState() => _AccessControlPanelState();
|
|
}
|
|
|
|
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
|
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
|
return switch (mode) {
|
|
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
|
AccessControlMode.rejectSelected => Icons.block_outlined,
|
|
};
|
|
}
|
|
|
|
String _getTextWithAccessControlMode(AccessControlMode mode) {
|
|
return switch (mode) {
|
|
AccessControlMode.acceptSelected => appLocalizations.whitelistMode,
|
|
AccessControlMode.rejectSelected => appLocalizations.blacklistMode,
|
|
};
|
|
}
|
|
|
|
String _getTextWithAccessSortType(AccessSortType type) {
|
|
return switch (type) {
|
|
AccessSortType.none => appLocalizations.defaultText,
|
|
AccessSortType.name => appLocalizations.name,
|
|
AccessSortType.time => appLocalizations.time,
|
|
};
|
|
}
|
|
|
|
IconData _getIconWithProxiesSortType(AccessSortType type) {
|
|
return switch (type) {
|
|
AccessSortType.none => Icons.sort,
|
|
AccessSortType.name => Icons.sort_by_alpha,
|
|
AccessSortType.time => Icons.timeline,
|
|
};
|
|
}
|
|
|
|
List<Widget> _buildModeSetting() {
|
|
return generateSection(
|
|
title: appLocalizations.mode,
|
|
items: [
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
scrollDirection: Axis.horizontal,
|
|
child: Consumer(
|
|
builder: (_, ref, __) {
|
|
final accessControlMode = ref.watch(
|
|
vpnSettingProvider.select((state) => state.accessControl.mode),
|
|
);
|
|
return Wrap(
|
|
spacing: 16,
|
|
children: [
|
|
for (final item in AccessControlMode.values)
|
|
SettingInfoCard(
|
|
Info(
|
|
label: _getTextWithAccessControlMode(item),
|
|
iconData: _getIconWithAccessControlMode(item),
|
|
),
|
|
isSelected: accessControlMode == item,
|
|
onPressed: () {
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
mode: item,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
],
|
|
);
|
|
},
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildSortSetting() {
|
|
return generateSection(
|
|
title: appLocalizations.sort,
|
|
items: [
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
scrollDirection: Axis.horizontal,
|
|
child: Consumer(
|
|
builder: (_, ref, __) {
|
|
final accessSortType = ref.watch(
|
|
vpnSettingProvider.select((state) => state.accessControl.sort),
|
|
);
|
|
return Wrap(
|
|
spacing: 16,
|
|
children: [
|
|
for (final item in AccessSortType.values)
|
|
SettingInfoCard(
|
|
Info(
|
|
label: _getTextWithAccessSortType(item),
|
|
iconData: _getIconWithProxiesSortType(item),
|
|
),
|
|
isSelected: accessSortType == item,
|
|
onPressed: () {
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
sort: item,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildSourceSetting() {
|
|
return generateSection(
|
|
title: appLocalizations.source,
|
|
items: [
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
scrollDirection: Axis.horizontal,
|
|
child: Consumer(
|
|
builder: (_, ref, __) {
|
|
final vm2 = ref.watch(
|
|
vpnSettingProvider.select(
|
|
(state) => VM2(
|
|
a: state.accessControl.isFilterSystemApp,
|
|
b: state.accessControl.isFilterNonInternetApp,
|
|
),
|
|
),
|
|
);
|
|
return Wrap(
|
|
spacing: 16,
|
|
children: [
|
|
SettingTextCard(
|
|
appLocalizations.systemApp,
|
|
isSelected: vm2.a == false,
|
|
onPressed: () {
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
isFilterSystemApp: !vm2.a,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
SettingTextCard(
|
|
appLocalizations.noNetworkApp,
|
|
isSelected: vm2.b == false,
|
|
onPressed: () {
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.accessControl(
|
|
isFilterNonInternetApp: !vm2.b,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
],
|
|
);
|
|
},
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
_copyToClipboard() async {
|
|
await globalState.safeRun(() {
|
|
final data = globalState.config.vpnProps.accessControl.toJson();
|
|
Clipboard.setData(
|
|
ClipboardData(
|
|
text: json.encode(data),
|
|
),
|
|
);
|
|
});
|
|
if (!mounted) return;
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
_pasteToClipboard() async {
|
|
await globalState.safeRun(
|
|
() async {
|
|
final data = await Clipboard.getData('text/plain');
|
|
final text = data?.text;
|
|
if (text == null) return;
|
|
ref.read(vpnSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith(
|
|
accessControl: AccessControl.fromJson(
|
|
json.decode(text),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
if (!mounted) return;
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
List<Widget> _buildActionSetting() {
|
|
return generateSection(
|
|
title: appLocalizations.action,
|
|
items: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
),
|
|
child: Wrap(
|
|
runSpacing: 16,
|
|
spacing: 16,
|
|
children: [
|
|
CommonChip(
|
|
avatar: const Icon(Icons.auto_awesome),
|
|
label: appLocalizations.intelligentSelected,
|
|
onPressed: () {
|
|
Navigator.of(context).pop(1);
|
|
},
|
|
),
|
|
CommonChip(
|
|
avatar: const Icon(Icons.paste),
|
|
label: appLocalizations.clipboardImport,
|
|
onPressed: _pasteToClipboard,
|
|
),
|
|
CommonChip(
|
|
avatar: const Icon(Icons.content_copy),
|
|
label: appLocalizations.clipboardExport,
|
|
onPressed: _copyToClipboard,
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
..._buildModeSetting(),
|
|
..._buildSortSetting(),
|
|
..._buildSourceSetting(),
|
|
..._buildActionSetting(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|