Files
MWClash/lib/views/profiles/overwrite/custom.dart
chen08209 8296302211 cache
2026-03-10 14:25:00 +08:00

738 lines
24 KiB
Dart

part of 'overwrite.dart';
class _CustomContent extends ConsumerWidget {
final int profileId;
const _CustomContent(this.profileId);
void _handleUseDefault() async {
final configMap = await coreController.getConfig(profileId);
final clashConfig = ClashConfig.fromJson(configMap);
await database.setProfileCustomData(
profileId,
clashConfig.proxyGroups,
clashConfig.rules,
);
}
void _handleToProxyGroupsView(BuildContext context) {
BaseNavigator.push(context, _CustomProxyGroupsView(profileId));
}
void _handleToRulesView(BuildContext context) {
BaseNavigator.push(context, _CustomRulesView(profileId));
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final proxyGroupNum =
ref.watch(proxyGroupsCountProvider(profileId)).value ?? -1;
final ruleNum = ref.watch(customRulesCountProvider(profileId)).value ?? -1;
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
SliverToBoxAdapter(
child: Column(
children: [InfoHeader(info: Info(label: '自定义'))],
),
),
SliverToBoxAdapter(child: SizedBox(height: 8)),
SliverToBoxAdapter(
child: _MoreActionButton(
label: '代理组',
onPressed: () {
_handleToProxyGroupsView(context);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
constraints: BoxConstraints(minWidth: 44),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
textAlign: TextAlign.center,
'$proxyGroupNum',
style: context.textTheme.bodySmall,
),
),
),
),
),
),
SliverToBoxAdapter(child: SizedBox(height: 4)),
SliverToBoxAdapter(
child: _MoreActionButton(
label: '规则',
onPressed: () {
_handleToRulesView(context);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
constraints: BoxConstraints(minWidth: 44),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
'$ruleNum',
style: context.textTheme.bodySmall,
textAlign: TextAlign.center,
),
),
),
),
),
SliverToBoxAdapter(child: SizedBox(height: 32)),
if (proxyGroupNum == 0 && ruleNum == 0)
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: MaterialBanner(
elevation: 0,
dividerColor: Colors.transparent,
content: Text('检测到没有数据'),
actions: [
CommonMinFilledButtonTheme(
child: FilledButton.tonal(
onPressed: _handleUseDefault,
child: Text('一键填入'),
),
),
],
),
),
),
// SliverToBoxAdapter(child: SizedBox(height: 8)),
// SliverToBoxAdapter(
// child: Padding(
// padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// child: CommonCard(
// radius: 18,
// child: ListTile(
// minTileHeight: 0,
// minVerticalPadding: 0,
// titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
// contentPadding: const EdgeInsets.symmetric(
// horizontal: 16,
// vertical: 16,
// ),
// title: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Flexible(
// child: Text('自定义规则', style: context.textTheme.bodyLarge),
// ),
// Icon(Icons.arrow_forward_ios, size: 18),
// ],
// ),
// ),
// onPressed: () {},
// ),
// ),
// ),
],
);
}
}
class _CustomProxyGroupsView extends ConsumerWidget {
final int profileId;
const _CustomProxyGroupsView(this.profileId);
void _handleReorder(WidgetRef ref, int oldIndex, int newIndex) {
ref.read(proxyGroupsProvider(profileId).notifier).order(oldIndex, newIndex);
}
void _handleEditProxyGroup(BuildContext context, ProxyGroup proxyGroup) {
showSheet(
context: context,
props: SheetProps(
isScrollControlled: true,
backgroundColor: Colors.transparent,
maxWidth: 400,
),
builder: (context) {
return _EditCustomProxyGroupNestedSheet(proxyGroup);
},
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final proxyGroups = ref.watch(proxyGroupsProvider(profileId)).value ?? [];
return CommonScaffold(
title: '代理组',
body: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: EdgeInsets.only(bottom: 16),
itemBuilder: (_, index) {
final proxyGroup = proxyGroups[index];
return ReorderableDelayedDragStartListener(
key: ValueKey(proxyGroup),
index: index,
child: Container(
margin: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: CommonCard(
radius: 16,
padding: EdgeInsets.all(16),
onPressed: () {
_handleEditProxyGroup(context, proxyGroup);
},
child: ListTile(
minTileHeight: 0,
minVerticalPadding: 0,
titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
title: Text(proxyGroup.name),
subtitle: Text(proxyGroup.type.name),
),
),
),
);
},
itemCount: proxyGroups.length,
onReorder: (oldIndex, newIndex) {
_handleReorder(ref, oldIndex, newIndex);
},
),
);
}
}
class _EditCustomProxyGroupNestedSheet extends StatelessWidget {
final ProxyGroup proxyGroup;
const _EditCustomProxyGroupNestedSheet(this.proxyGroup);
Future<void> _handlePop(
BuildContext context,
NavigatorState? navigatorState,
) async {
if (navigatorState != null && navigatorState.canPop()) {
final res = await globalState.showMessage(
message: TextSpan(text: '确定要退出当前窗口吗?'),
);
if (res != true) {
return;
}
}
if (context.mounted) {
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final GlobalKey<NavigatorState> nestedNavigatorKey = GlobalKey();
final nestedNavigator = Navigator(
key: nestedNavigatorKey,
onGenerateInitialRoutes: (navigator, initialRoute) {
return [
PagedSheetRoute(
builder: (context) {
return _EditCustomProxyGroupView(proxyGroup);
},
),
];
},
);
final sheetProvider = SheetProvider.of(context);
return CommonPopScope(
onPop: (_) async {
_handlePop(context, nestedNavigatorKey.currentState);
return false;
},
child: sheetProvider!.copyWith(
nestedNavigatorPopCallback: () {
Navigator.of(context).pop();
},
child: Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: () async {
_handlePop(context, nestedNavigatorKey.currentState);
},
),
),
SheetViewport(
child: PagedSheet(
decoration: MaterialSheetDecoration(
size: SheetSize.stretch,
borderRadius: sheetProvider.type == SheetType.bottomSheet
? BorderRadius.vertical(top: Radius.circular(28))
: BorderRadius.zero,
clipBehavior: Clip.antiAlias,
),
navigator: nestedNavigator,
),
),
],
),
),
);
}
}
class _EditCustomProxyGroupView extends ConsumerStatefulWidget {
final ProxyGroup proxyGroup;
const _EditCustomProxyGroupView(this.proxyGroup);
@override
ConsumerState createState() => _EditCustomProxyGroupViewState();
}
class _EditCustomProxyGroupViewState
extends ConsumerState<_EditCustomProxyGroupView> {
final _nameController = TextEditingController();
final _hideController = ValueNotifier<bool>(false);
final _disableUDPController = ValueNotifier<bool>(false);
final _proxiesController = ValueNotifier<List<String>>([]);
final _useController = ValueNotifier<List<String>>([]);
final _typeController = ValueNotifier<GroupType>(GroupType.Selector);
final _allProxiesController = ValueNotifier<bool>(false);
final _allProviderController = ValueNotifier<bool>(false);
@override
void initState() {
super.initState();
final proxyGroup = widget.proxyGroup;
_nameController.text = proxyGroup.name;
_hideController.value = proxyGroup.hidden ?? false;
_disableUDPController.value = proxyGroup.disableUDP ?? false;
_typeController.value = proxyGroup.type;
_proxiesController.value = proxyGroup.proxies ?? [];
_useController.value = proxyGroup.use ?? [];
if (proxyGroup.includeAll == true) {
_allProxiesController.value = true;
_allProviderController.value = true;
} else {
_allProxiesController.value = proxyGroup.includeAllProxies ?? false;
_allProviderController.value = proxyGroup.includeAllProviders ?? false;
}
}
@override
void dispose() {
_nameController.dispose();
_hideController.dispose();
_disableUDPController.dispose();
_typeController.dispose();
_proxiesController.dispose();
_useController.dispose();
_allProxiesController.dispose();
_allProviderController.dispose();
super.dispose();
}
Future<void> _showTypeOptions() async {
final value = await globalState.showCommonDialog<GroupType>(
child: OptionsDialog<GroupType>(
title: '类型',
options: GroupType.values,
textBuilder: (item) => item.name,
value: _typeController.value,
),
);
if (value == null) {
return;
}
_typeController.value = value;
}
Widget _buildItem({
required Widget title,
Widget? trailing,
final VoidCallback? onPressed,
}) {
return CommonInputListItem(
onPressed: onPressed,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 16,
children: [
title,
if (trailing != null)
Flexible(
child: IconTheme(
data: IconThemeData(
size: 16,
color: context.colorScheme.onSurface.opacity60,
),
child: Container(
alignment: Alignment.centerRight,
height: globalState.measure.bodyLargeHeight + 24,
child: trailing,
),
),
),
],
),
);
}
void _handleToProxies() {
final isBottomSheet =
SheetProvider.of(context)?.type == SheetType.bottomSheet;
Navigator.of(context).push(
PagedSheetRoute(
builder: (context) => SizedBox(
height: isBottomSheet
? appController.viewSize.height * 0.85
: double.maxFinite,
child: AdaptiveSheetScaffold(
title: '选择代理',
body: Center(child: Text('123')),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final isBottomSheet =
SheetProvider.of(context)?.type == SheetType.bottomSheet;
return AdaptiveSheetScaffold(
actions: [IconButtonData(icon: Icons.check, onPressed: () {})],
body: SizedBox(
height: isBottomSheet
? appController.viewSize.height * 0.65
: double.maxFinite,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 20),
children: [
generateSectionV3(
title: '通用',
items: [
_buildItem(
title: Text('名称'),
trailing: TextFormField(
controller: _nameController,
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '输入代理组名称',
),
),
),
_buildItem(
title: Text('类型'),
onPressed: () {
_showTypeOptions();
},
trailing: ValueListenableBuilder(
valueListenable: _typeController,
builder: (_, type, _) {
return Text(type.name);
},
),
),
_buildItem(title: Text('图标')),
_buildItem(
title: Text('从列表中隐藏'),
onPressed: () {
_hideController.value = !_hideController.value;
},
trailing: ValueListenableBuilder(
valueListenable: _hideController,
builder: (_, value, _) {
return Switch(
value: value,
onChanged: (value) {
_hideController.value = value;
},
);
},
),
),
_buildItem(
title: Text('禁用UDP'),
onPressed: () {
_disableUDPController.value = !_disableUDPController.value;
},
trailing: ValueListenableBuilder(
valueListenable: _disableUDPController,
builder: (_, value, _) {
return Switch(
value: value,
onChanged: (value) {
_disableUDPController.value = value;
},
);
},
),
),
],
),
generateSectionV3(
title: '节点',
items: [
_buildItem(
title: Text('选择代理'),
trailing: ValueListenableBuilder(
valueListenable: _proxiesController,
builder: (_, proxies, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
child: Container(
constraints: BoxConstraints(minWidth: 32),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 4,
vertical: 3,
),
child: Text(
textAlign: TextAlign.center,
'${proxies.length}',
style: context.textTheme.bodySmall,
),
),
),
),
Icon(Icons.arrow_forward_ios),
],
);
},
),
onPressed: _handleToProxies,
),
_buildItem(
title: Text('选择代理集'),
trailing: Icon(Icons.arrow_forward_ios),
),
_buildItem(
title: Text('节点过滤器'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
_buildItem(
title: Text('排除过滤器'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
_buildItem(
title: Text('排除类型'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
_buildItem(
title: Text('预期状态'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
],
),
generateSectionV3(
title: '其他',
items: [
_buildItem(
title: Text('测速链接'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
_buildItem(
title: Text('最大失败次数'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
_buildItem(
title: Text('使用时测速'),
trailing: Switch(value: false, onChanged: (_) {}),
),
_buildItem(
title: Text('测速间隔'),
trailing: TextFormField(
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: NoInputBorder(),
hintText: '可选',
),
),
),
],
),
generateSectionV3(
title: '操作',
items: [
_buildItem(
title: Text('删除'),
onPressed: () {
_disableUDPController.value = !_disableUDPController.value;
},
),
],
),
],
),
),
title: '编辑代理组',
);
}
}
class _CustomRulesView extends ConsumerStatefulWidget {
final int profileId;
const _CustomRulesView(this.profileId);
@override
ConsumerState createState() => _CustomRulesViewState();
}
class _CustomRulesViewState extends ConsumerState<_CustomRulesView> {
final _key = utils.id;
void _handleReorder(int oldIndex, int newIndex) {
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.order(oldIndex, newIndex);
}
void _handleSelected(int ruleId) {
ref.read(selectedItemsProvider(_key).notifier).update((selectedRules) {
final newSelectedRules = Set<int>.from(selectedRules)
..addOrRemove(ruleId);
return newSelectedRules;
});
}
void _handleSelectAll() {
final ids =
ref
.read(profileCustomRulesProvider(widget.profileId))
.value
?.map((item) => item.id)
.toSet() ??
{};
ref.read(selectedItemsProvider(_key).notifier).update((selected) {
return selected.containsAll(ids) ? {} : ids;
});
}
Future<void> _handleDelete() async {
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.deleteMultipTip(appLocalizations.rule),
),
);
if (res != true) {
return;
}
final selectedRules = ref.read(selectedItemsProvider(_key));
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.delAll(selectedRules.cast<int>());
ref.read(selectedItemsProvider(_key).notifier).value = {};
}
@override
void initState() {
super.initState();
}
@override
Widget build(context) {
final rules =
ref.watch(profileCustomRulesProvider(widget.profileId)).value ?? [];
final selectedRules = ref.watch(selectedItemsProvider(_key));
return CommonScaffold(
title: appLocalizations.rule,
actions: [
if (selectedRules.isNotEmpty) ...[
CommonMinIconButtonTheme(
child: IconButton.filledTonal(
onPressed: _handleDelete,
icon: Icon(Icons.delete),
),
),
SizedBox(width: 2),
],
CommonMinFilledButtonTheme(
child: selectedRules.isNotEmpty
? FilledButton(
onPressed: _handleSelectAll,
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
// _handleAddOrUpdate();
},
child: Text(appLocalizations.add),
),
),
SizedBox(width: 8),
],
body: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemBuilder: (_, index) {
final rule = rules[index];
return ReorderableDelayedDragStartListener(
key: ObjectKey(rule),
index: index,
child: RuleItem(
isEditing: selectedRules.isNotEmpty,
isSelected: selectedRules.contains(rule.id),
rule: rule,
onSelected: () {
_handleSelected(rule.id);
},
onEdit: (rule) {
// _handleAddOrUpdate(rule);
},
),
);
},
itemCount: rules.length,
onReorder: _handleReorder,
),
);
}
}