Files
FlClash/lib/features/form/rule_preset.dart
T
chen08209 9bd2286eb5 feat(ui): layer surfaces with fills and follow the iOS type scale
Changelog: iOS-style surfaces, alerts and sheets; clearer TV focus
2026-10-17 17:21:43 +08:00

146 lines
4.5 KiB
Dart

import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
enum RulePreset {
blockQuic(AppGlyphs.block, [
'AND,((NETWORK,UDP),(DST-PORT,443)),REJECT-DROP',
]),
blockStun(AppGlyphs.eyeOff, [
'AND,((NETWORK,UDP),(DST-PORT,3478/19302)),REJECT-DROP',
]),
blockDot(AppGlyphs.dns, ['DST-PORT,853,REJECT']),
// Connecting to 0.0.0.0 or :: also reaches a local listener.
blockLoopback(AppGlyphs.sync, [
'IP-CIDR,127.0.0.0/8,REJECT,no-resolve',
'IP-CIDR,0.0.0.0/32,REJECT,no-resolve',
'IP-CIDR6,::1/128,REJECT,no-resolve',
'IP-CIDR6,::/128,REJECT,no-resolve',
'AND,((IN-TYPE,TUN),(DST-PORT,853)),REJECT',
]),
lanDirect(AppGlyphs.wifi, [
'GEOSITE,private,DIRECT',
'GEOIP,LAN,DIRECT,no-resolve',
]),
systemServicesDirect(AppGlyphs.devices, [
'GEOSITE,apple,DIRECT',
'GEOSITE,microsoft,DIRECT',
]),
bittorrentDirect(AppGlyphs.cloudDownload, [
'GEOSITE,category-public-tracker,DIRECT',
'GEOSITE,category-pt,DIRECT',
r'PROCESS-NAME-REGEX,(?i)^(qbittorrent|transmission.*|deluge.*|aria2c|motrix|utorrent|bitcomet)(\.exe)?$,DIRECT',
]);
final Glyph glyph;
final List<String> rawRules;
const RulePreset(this.glyph, this.rawRules);
String label(AppLocalizations appLocalizations) => switch (this) {
RulePreset.blockQuic => appLocalizations.rulePresetBlockQuic,
RulePreset.blockStun => appLocalizations.rulePresetBlockStun,
RulePreset.blockDot => appLocalizations.rulePresetBlockDot,
RulePreset.blockLoopback => appLocalizations.rulePresetBlockLoopback,
RulePreset.lanDirect => appLocalizations.rulePresetLanDirect,
RulePreset.systemServicesDirect =>
appLocalizations.rulePresetSystemServicesDirect,
RulePreset.bittorrentDirect => appLocalizations.rulePresetBittorrentDirect,
};
List<Rule> get rules => rawRules.map(Rule.parse).toList();
}
class RulePresetSheet extends StatefulWidget {
final ValueChanged<List<Rule>> onAdd;
const RulePresetSheet({super.key, required this.onAdd});
@override
State<RulePresetSheet> createState() => _RulePresetSheetState();
}
class _RulePresetSheetState extends State<RulePresetSheet> {
final _selected = <RulePreset>{};
void _handleToggle(RulePreset preset) {
setState(() {
_selected.addOrRemove(preset);
});
}
void _handleConfirm() {
final rules = [
for (final preset in RulePreset.values)
if (_selected.contains(preset)) ...preset.rules,
];
Navigator.of(context).pop();
widget.onAdd(rules);
}
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
const presets = RulePreset.values;
return CommonScaffold(
title: appLocalizations.quickActions,
confirmationAction: _selected.isEmpty
? null
: IconButtonData(
glyph: AppGlyphs.check,
onPressed: _handleConfirm,
tooltip: appLocalizations.confirm,
),
body: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 16,
).copyWith(top: context.contentTopPadding, bottom: 20),
itemCount: presets.length,
itemBuilder: (context, index) {
final preset = presets[index];
final isSelected = _selected.contains(preset);
return ItemPositionProvider(
position: ItemPosition.get(index, presets.length),
child: DecorationListItem(
leading: GlyphIcon(preset.glyph),
title: Text(preset.label(appLocalizations)),
subtitle: _PresetSubtitle(preset),
isSelected: isSelected,
trailing: isSelected ? const SelectionCheck() : null,
onPressed: () => _handleToggle(preset),
),
);
},
),
);
}
}
class _PresetSubtitle extends StatelessWidget {
final RulePreset preset;
const _PresetSubtitle(this.preset);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final raw in preset.rawRules)
Text(
raw,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyle.footnote
.of(context)
.toJetBrainsMono
.copyWith(color: context.colorScheme.tertiary),
),
],
);
}
}