This commit is contained in:
chen08209
2026-03-10 19:24:23 +08:00
parent 8296302211
commit 3f5e7b80bc
12 changed files with 1234 additions and 900 deletions

View File

@@ -83,6 +83,14 @@ class ProxyGroupsDao extends DatabaseAccessor<Database>
return stmt.count;
}
Selectable<ProxyGroup> get(int profileId, String name) {
final stmt = proxyGroups.select();
stmt.where(
(row) => row.profileId.equals(profileId) & row.name.equals(name),
);
return stmt.map((item) => item.toProxyGroup());
}
Future<int> order(
int profileId, {
required ProxyGroup proxyGroup,

View File

@@ -333,6 +333,17 @@ class ProxyGroups extends _$ProxyGroups with AsyncNotifierMixin {
List<ProxyGroup> get value => state.value ?? [];
}
@Riverpod(name: 'proxyGroupProvider')
class ProxyGroupProvider extends _$ProxyGroupProvider with AsyncNotifierMixin {
@override
Stream<ProxyGroup> build(int profileId, String name) {
return database.proxyGroupsDao.get(profileId, name).watchSingle();
}
@override
ProxyGroup get value => state.requireValue;
}
@riverpod
class ProfileDisabledRuleIds extends _$ProfileDisabledRuleIds
with AsyncNotifierMixin {

View File

@@ -749,6 +749,98 @@ abstract class _$ProxyGroups extends $StreamNotifier<List<ProxyGroup>> {
}
}
@ProviderFor(ProxyGroupProvider)
const proxyGroupProvider = ProxyGroupProviderFamily._();
final class ProxyGroupProviderProvider
extends $StreamNotifierProvider<ProxyGroupProvider, ProxyGroup> {
const ProxyGroupProviderProvider._({
required ProxyGroupProviderFamily super.from,
required (int, String) super.argument,
}) : super(
retry: null,
name: r'proxyGroupProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$proxyGroupProviderHash();
@override
String toString() {
return r'proxyGroupProvider'
''
'$argument';
}
@$internal
@override
ProxyGroupProvider create() => ProxyGroupProvider();
@override
bool operator ==(Object other) {
return other is ProxyGroupProviderProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$proxyGroupProviderHash() =>
r'60ec3f8a2a09e3ed1ac73d724b1ade295309897d';
final class ProxyGroupProviderFamily extends $Family
with
$ClassFamilyOverride<
ProxyGroupProvider,
AsyncValue<ProxyGroup>,
ProxyGroup,
Stream<ProxyGroup>,
(int, String)
> {
const ProxyGroupProviderFamily._()
: super(
retry: null,
name: r'proxyGroupProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ProxyGroupProviderProvider call(int profileId, String name) =>
ProxyGroupProviderProvider._(argument: (profileId, name), from: this);
@override
String toString() => r'proxyGroupProvider';
}
abstract class _$ProxyGroupProvider extends $StreamNotifier<ProxyGroup> {
late final _$args = ref.$arg as (int, String);
int get profileId => _$args.$1;
String get name => _$args.$2;
Stream<ProxyGroup> build(int profileId, String name);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args.$1, _$args.$2);
final ref = this.ref as $Ref<AsyncValue<ProxyGroup>, ProxyGroup>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<ProxyGroup>, ProxyGroup>,
AsyncValue<ProxyGroup>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(ProfileDisabledRuleIds)
const profileDisabledRuleIdsProvider = ProfileDisabledRuleIdsFamily._();

View File

@@ -2443,6 +2443,81 @@ final class ScriptFamily extends $Family
String toString() => r'scriptProvider';
}
@ProviderFor(clashConfig)
const clashConfigProvider = ClashConfigFamily._();
final class ClashConfigProvider
extends
$FunctionalProvider<
AsyncValue<ClashConfig>,
ClashConfig,
FutureOr<ClashConfig>
>
with $FutureModifier<ClashConfig>, $FutureProvider<ClashConfig> {
const ClashConfigProvider._({
required ClashConfigFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'clashConfigProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clashConfigHash();
@override
String toString() {
return r'clashConfigProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<ClashConfig> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<ClashConfig> create(Ref ref) {
final argument = this.argument as int;
return clashConfig(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ClashConfigProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$clashConfigHash() => r'3987f6aee1131fe9b3978b914372b04ca2ae773c';
final class ClashConfigFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<ClashConfig>, int> {
const ClashConfigFamily._()
: super(
retry: null,
name: r'clashConfigProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ClashConfigProvider call(int profileId) =>
ClashConfigProvider._(argument: profileId, from: this);
@override
String toString() => r'clashConfigProvider';
}
@ProviderFor(setupState)
const setupStateProvider = SetupStateFamily._();

View File

@@ -1,5 +1,6 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/core/controller.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -677,6 +678,13 @@ Future<Script?> script(Ref ref, int? scriptId) async {
return script;
}
@riverpod
Future<ClashConfig> clashConfig(Ref ref, int profileId) async {
final configMap = await coreController.getConfig(profileId);
final clashConfig = ClashConfig.fromJson(configMap);
return clashConfig;
}
@riverpod
Future<SetupState> setupState(Ref ref, int? profileId) async {
final profile = ref.watch(profileProvider(profileId));

View File

@@ -1,13 +1,10 @@
part of 'overwrite.dart';
class _CustomContent extends ConsumerWidget {
final int profileId;
const _CustomContent();
const _CustomContent(this.profileId);
void _handleUseDefault() async {
final configMap = await coreController.getConfig(profileId);
final clashConfig = ClashConfig.fromJson(configMap);
void _handleUseDefault(WidgetRef ref, int profileId) async {
final clashConfig = await ref.read(clashConfigProvider(profileId).future);
await database.setProfileCustomData(
profileId,
clashConfig.proxyGroups,
@@ -15,19 +12,28 @@ class _CustomContent extends ConsumerWidget {
);
}
void _handleToProxyGroupsView(BuildContext context) {
void _handleToProxyGroupsView(BuildContext context, int profileId) {
BaseNavigator.push(context, _CustomProxyGroupsView(profileId));
}
void _handleToRulesView(BuildContext context) {
void _handleToRulesView(BuildContext context, int profileId) {
BaseNavigator.push(context, _CustomRulesView(profileId));
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileId = ProfileIdProvider.of(context)!.profileId;
final proxyGroupNum =
ref.watch(proxyGroupsCountProvider(profileId)).value ?? -1;
final ruleNum = ref.watch(customRulesCountProvider(profileId)).value ?? -1;
final hasDefault = ref.watch(
clashConfigProvider(profileId).select((state) {
final clashConfig = state.value;
return ((clashConfig?.proxyGroups.length ?? 0) +
(clashConfig?.rules.length ?? 0)) >
0;
}),
);
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
@@ -41,7 +47,7 @@ class _CustomContent extends ConsumerWidget {
child: _MoreActionButton(
label: '代理组',
onPressed: () {
_handleToProxyGroupsView(context);
_handleToProxyGroupsView(context, profileId);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
@@ -66,7 +72,7 @@ class _CustomContent extends ConsumerWidget {
child: _MoreActionButton(
label: '规则',
onPressed: () {
_handleToRulesView(context);
_handleToRulesView(context, profileId);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
@@ -85,7 +91,7 @@ class _CustomContent extends ConsumerWidget {
),
),
SliverToBoxAdapter(child: SizedBox(height: 32)),
if (proxyGroupNum == 0 && ruleNum == 0)
if (proxyGroupNum == 0 && ruleNum == 0 && hasDefault)
SliverFillRemaining(
hasScrollBody: false,
child: Align(
@@ -97,7 +103,9 @@ class _CustomContent extends ConsumerWidget {
actions: [
CommonMinFilledButtonTheme(
child: FilledButton.tonal(
onPressed: _handleUseDefault,
onPressed: () {
_handleUseDefault(ref, profileId);
},
child: Text('一键填入'),
),
),
@@ -105,517 +113,11 @@ class _CustomContent extends ConsumerWidget {
),
),
),
// 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;
@@ -628,9 +130,16 @@ class _CustomRulesView extends ConsumerStatefulWidget {
class _CustomRulesViewState extends ConsumerState<_CustomRulesView> {
final _key = utils.id;
int get _profileId => widget.profileId;
@override
void initState() {
super.initState();
}
void _handleReorder(int oldIndex, int newIndex) {
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.read(profileCustomRulesProvider(_profileId).notifier)
.order(oldIndex, newIndex);
}
@@ -645,7 +154,7 @@ class _CustomRulesViewState extends ConsumerState<_CustomRulesView> {
void _handleSelectAll() {
final ids =
ref
.read(profileCustomRulesProvider(widget.profileId))
.read(profileCustomRulesProvider(_profileId))
.value
?.map((item) => item.id)
.toSet() ??
@@ -667,20 +176,14 @@ class _CustomRulesViewState extends ConsumerState<_CustomRulesView> {
}
final selectedRules = ref.read(selectedItemsProvider(_key));
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.read(profileCustomRulesProvider(_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 rules = ref.watch(profileCustomRulesProvider(_profileId)).value ?? [];
final selectedRules = ref.watch(selectedItemsProvider(_key));
return CommonScaffold(
title: appLocalizations.rule,

View File

@@ -0,0 +1,592 @@
part of 'overwrite.dart';
class _CustomProxyGroupsView extends ConsumerWidget {
final int profileId;
const _CustomProxyGroupsView(this.profileId);
void _handleReorder(
WidgetRef ref,
int profileId,
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 ProxyGroupProvider(
proxyGroup: proxyGroup,
child: _EditProxyGroupNestedSheet(),
);
},
);
}
@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, profileId, oldIndex, newIndex);
},
),
);
}
}
class _EditProxyGroupNestedSheet extends StatelessWidget {
const _EditProxyGroupNestedSheet();
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 _EditProxyGroupView();
},
),
];
},
);
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 _EditProxyGroupView extends ConsumerStatefulWidget {
const _EditProxyGroupView();
@override
ConsumerState createState() => _EditProxyGroupViewState();
}
class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> {
late ProxyGroup _proxyGroup;
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();
WidgetsBinding.instance.addPostFrameCallback((_) {
_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 didChangeDependencies() {
super.didChangeDependencies();
_proxyGroup = ProxyGroupProvider.of(context)!.proxyGroup;
}
@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 _handleToProxiesView() {
Navigator.of(
context,
).push(PagedSheetRoute(builder: (context) => _EditProxiesView()));
}
void _handleToProvidersView() {}
Widget _buildProvidersItem() {
return _buildItem(
title: Text('选择代理集'),
trailing: ValueListenableBuilder(
valueListenable: _allProviderController,
builder: (_, allProviders, _) {
return ValueListenableBuilder(
valueListenable: _useController,
builder: (_, use, _) {
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 2,
children: [
!allProviders
? 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,
'${use.length}',
style: context.textTheme.bodySmall,
),
),
),
)
: Icon(
Icons.check_circle_outline,
size: 20,
color: Colors.greenAccent.shade200,
),
Icon(Icons.arrow_forward_ios),
],
);
},
);
},
),
onPressed: _handleToProvidersView,
);
}
Widget _buildProxiesItem() {
return _buildItem(
title: Text('选择代理'),
trailing: ValueListenableBuilder(
valueListenable: _allProxiesController,
builder: (_, allProxies, _) {
return ValueListenableBuilder(
valueListenable: _proxiesController,
builder: (_, proxies, _) {
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 2,
children: [
!allProxies
? 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.check_circle_outline,
size: 20,
color: Colors.greenAccent.shade200,
),
Icon(Icons.arrow_forward_ios),
],
);
},
);
},
),
onPressed: _handleToProxiesView,
);
}
Widget _buildGroupTypeItem() {
return _buildItem(
title: Text('类型'),
onPressed: () {
_showTypeOptions();
},
trailing: ValueListenableBuilder(
valueListenable: _typeController,
builder: (_, type, _) {
return Text(type.name);
},
),
);
}
@override
Widget build(BuildContext context) {
_proxyGroup = ProxyGroupProvider.of(context)!.proxyGroup;
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: '输入代理组名称',
),
),
),
_buildGroupTypeItem(),
_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: [
_buildProxiesItem(),
_buildProvidersItem(),
_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 _EditProxiesView extends StatefulWidget {
const _EditProxiesView();
@override
State<_EditProxiesView> createState() => _EditProxiesViewState();
}
class _EditProxiesViewState extends State<_EditProxiesView> {
@override
Widget build(BuildContext context) {
final isBottomSheet =
SheetProvider.of(context)?.type == SheetType.bottomSheet;
return SizedBox(
height: isBottomSheet
? appController.viewSize.height * 0.85
: double.maxFinite,
child: AdaptiveSheetScaffold(
title: '选择代理',
body: CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 0,
).copyWith(top: 16),
sliver: SliverToBoxAdapter(
child: CommonCard(
radius: 20,
type: CommonCardType.filled,
child: ListItem.switchItem(
minTileHeight: 54,
title: Text('包含所有代理'),
delegate: SwitchDelegate(value: false, onChanged: (_) {}),
),
),
),
),
],
),
),
);
}
}

View File

@@ -18,6 +18,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:smooth_sheets/smooth_sheets.dart';
part 'custom.dart';
part 'custom_proxies.dart';
part 'script.dart';
part 'standard.dart';
part 'widgets.dart';
class OverwriteView extends ConsumerStatefulWidget {
@@ -45,19 +48,20 @@ class _OverwriteViewState extends ConsumerState<OverwriteView> {
@override
Widget build(BuildContext context) {
return CommonScaffold(
title: appLocalizations.override,
actions: [
CommonMinFilledButtonTheme(
child: FilledButton(
onPressed: _handlePreview,
child: Text(appLocalizations.preview),
return ProfileIdProvider(
profileId: widget.profileId,
child: CommonScaffold(
title: appLocalizations.override,
actions: [
CommonMinFilledButtonTheme(
child: FilledButton(
onPressed: _handlePreview,
child: Text(appLocalizations.preview),
),
),
),
SizedBox(width: 8),
],
body: CustomScrollView(
slivers: [_Title(widget.profileId), _Content(widget.profileId)],
SizedBox(width: 8),
],
body: CustomScrollView(slivers: [_Title(), _Content()]),
),
);
}
@@ -70,9 +74,7 @@ class _OverwriteViewState extends ConsumerState<OverwriteView> {
}
class _Title extends ConsumerWidget {
final int profileId;
const _Title(this.profileId);
const _Title();
String _getTitle(OverwriteType type) {
return switch (type) {
@@ -98,7 +100,7 @@ class _Title extends ConsumerWidget {
};
}
void _handleChangeType(WidgetRef ref, OverwriteType type) {
void _handleChangeType(WidgetRef ref, int profileId, OverwriteType type) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(overwriteType: type);
});
@@ -106,6 +108,7 @@ class _Title extends ConsumerWidget {
@override
Widget build(context, ref) {
final profileId = ProfileIdProvider.of(context)!.profileId;
final overwriteType = ref.watch(overwriteTypeProvider(profileId));
return SliverToBoxAdapter(
child: Column(
@@ -122,7 +125,7 @@ class _Title extends ConsumerWidget {
CommonCard(
isSelected: overwriteType == type,
onPressed: () {
_handleChangeType(ref, type);
_handleChangeType(ref, profileId, type);
},
child: Padding(
padding: const EdgeInsets.all(16),
@@ -157,365 +160,17 @@ class _Title extends ConsumerWidget {
}
class _Content extends ConsumerWidget {
final int profileId;
const _Content(this.profileId);
const _Content();
@override
Widget build(BuildContext context, ref) {
final profileId = ProfileIdProvider.of(context)!.profileId;
final overwriteType = ref.watch(overwriteTypeProvider(profileId));
ref.listen(clashConfigProvider(profileId), (_, _) {});
return switch (overwriteType) {
OverwriteType.standard => _StandardContent(profileId),
OverwriteType.script => _ScriptContent(profileId),
OverwriteType.custom => _CustomContent(profileId),
OverwriteType.standard => _StandardContent(),
OverwriteType.script => _ScriptContent(),
OverwriteType.custom => _CustomContent(),
};
}
}
class _StandardContent extends ConsumerStatefulWidget {
final int profileId;
const _StandardContent(this.profileId);
@override
ConsumerState createState() => _StandardContentState();
}
class _StandardContentState extends ConsumerState<_StandardContent> {
final _key = utils.id;
Future<void> _handleAddOrUpdate([Rule? rule]) async {
final res = await globalState.showCommonDialog<Rule>(
child: AddOrEditRuleDialog(rule: rule),
);
if (res == null) {
return;
}
ref.read(profileAddedRulesProvider(widget.profileId).notifier).put(res);
}
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(profileAddedRulesProvider(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(profileAddedRulesProvider(widget.profileId).notifier)
.delAll(selectedRules.cast<int>());
ref.read(selectedItemsProvider(_key).notifier).value = {};
}
@override
Widget build(BuildContext context) {
final addedRules =
ref.watch(profileAddedRulesProvider(widget.profileId)).value ?? [];
final selectedRules = ref.watch(selectedItemsProvider(_key));
return CommonPopScope(
onPop: (_) {
if (selectedRules.isNotEmpty) {
ref.read(selectedItemsProvider(_key).notifier).value = {};
return false;
}
Navigator.of(context).pop();
return false;
},
child: SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
SliverToBoxAdapter(
child: Column(
children: [
InfoHeader(
info: Info(label: appLocalizations.addedRules),
actions: [
if (selectedRules.isNotEmpty) ...[
CommonMinIconButtonTheme(
child: IconButton.filledTonal(
onPressed: () {
_handleDelete();
},
icon: Icon(Icons.delete),
),
),
SizedBox(width: 8),
],
CommonMinFilledButtonTheme(
child: selectedRules.isNotEmpty
? FilledButton(
onPressed: () {
_handleSelectAll();
},
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
_handleAddOrUpdate();
},
child: Text(appLocalizations.add),
),
),
],
),
],
),
),
SliverToBoxAdapter(child: SizedBox(height: 8)),
Consumer(
builder: (_, ref, _) {
return SliverReorderableList(
itemCount: addedRules.length,
itemBuilder: (_, index) {
final rule = addedRules[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);
},
),
);
},
onReorder: ref
.read(profileAddedRulesProvider(widget.profileId).notifier)
.order,
);
},
),
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(
appLocalizations.controlGlobalAddedRules,
style: context.textTheme.bodyLarge,
),
),
Icon(Icons.arrow_forward_ios, size: 18),
],
),
),
onPressed: () {
BaseNavigator.push(
context,
_EditGlobalAddedRules(profileId: widget.profileId),
);
},
),
),
),
],
),
);
}
}
class _ScriptContent extends ConsumerWidget {
final int profileId;
const _ScriptContent(this.profileId);
void _handleChange(WidgetRef ref, int scriptId) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(
scriptId: state.scriptId == scriptId ? null : scriptId,
);
});
}
@override
Widget build(BuildContext context, ref) {
final scriptId = ref.watch(
profileProvider(profileId).select((state) => state?.scriptId),
);
final scripts = ref.watch(scriptsProvider).value ?? [];
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
SliverToBoxAdapter(
child: Column(
children: [
InfoHeader(info: Info(label: appLocalizations.overrideScript)),
],
),
),
SliverToBoxAdapter(child: SizedBox(height: 8)),
Consumer(
builder: (_, ref, _) {
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.builder(
itemCount: scripts.length,
itemBuilder: (_, index) {
final script = scripts[index];
return Container(
margin: EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
radius: 18,
child: ListTile(
minLeadingWidth: 0,
minTileHeight: 0,
minVerticalPadding: 16,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
),
title: Row(
children: [
SizedBox(
width: 24,
height: 24,
child: Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
toggleable: true,
value: script.id,
groupValue: scriptId,
onChanged: (_) {
_handleChange(ref, script.id);
},
),
),
SizedBox(width: 8),
Flexible(child: Text(script.label)),
],
),
onTap: () {
_handleChange(ref, script.id);
},
),
),
);
},
),
);
},
),
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(
appLocalizations.goToConfigureScript,
style: context.textTheme.bodyLarge,
),
),
Icon(Icons.arrow_forward_ios, size: 18),
],
),
),
onPressed: () {
BaseNavigator.push(context, const ScriptsView());
},
),
),
),
],
);
}
}
class _EditGlobalAddedRules extends ConsumerWidget {
final int profileId;
const _EditGlobalAddedRules({required this.profileId});
void _handleChange(WidgetRef ref, bool status, int ruleId) {
if (status) {
ref.read(profileDisabledRuleIdsProvider(profileId).notifier).put(ruleId);
} else {
ref.read(profileDisabledRuleIdsProvider(profileId).notifier).del(ruleId);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final disabledRuleIds =
ref.watch(profileDisabledRuleIdsProvider(profileId)).value ?? [];
final rules = ref.watch(globalRulesProvider).value ?? [];
return BaseScaffold(
title: appLocalizations.editGlobalRules,
body: rules.isEmpty
? NullStatus(
label: appLocalizations.nullTip(appLocalizations.rule),
illustration: RuleEmptyIllustration(),
)
: ListView.builder(
padding: EdgeInsets.all(16),
itemBuilder: (context, index) {
final rule = rules[index];
return RuleStatusItem(
status: !disabledRuleIds.contains(rule.id),
rule: rule,
onChange: (status) {
_handleChange(ref, !status, rule.id);
},
);
},
itemCount: rules.length,
),
);
}
}

View File

@@ -0,0 +1,120 @@
part of 'overwrite.dart';
class _ScriptContent extends ConsumerWidget {
const _ScriptContent();
void _handleChange(WidgetRef ref, int profileId, int scriptId) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(
scriptId: state.scriptId == scriptId ? null : scriptId,
);
});
}
@override
Widget build(BuildContext context, ref) {
final profileId = ProfileIdProvider.of(context)!.profileId;
final scriptId = ref.watch(
profileProvider(profileId).select((state) => state?.scriptId),
);
final scripts = ref.watch(scriptsProvider).value ?? [];
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
SliverToBoxAdapter(
child: Column(
children: [
InfoHeader(info: Info(label: appLocalizations.overrideScript)),
],
),
),
SliverToBoxAdapter(child: SizedBox(height: 8)),
Consumer(
builder: (_, ref, _) {
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.builder(
itemCount: scripts.length,
itemBuilder: (_, index) {
final script = scripts[index];
return Container(
margin: EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
radius: 18,
child: ListTile(
minLeadingWidth: 0,
minTileHeight: 0,
minVerticalPadding: 16,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
),
title: Row(
children: [
SizedBox(
width: 24,
height: 24,
child: Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
toggleable: true,
value: script.id,
groupValue: scriptId,
onChanged: (_) {
_handleChange(ref, profileId, script.id);
},
),
),
SizedBox(width: 8),
Flexible(child: Text(script.label)),
],
),
onTap: () {
_handleChange(ref, profileId, script.id);
},
),
),
);
},
),
);
},
),
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(
appLocalizations.goToConfigureScript,
style: context.textTheme.bodyLarge,
),
),
Icon(Icons.arrow_forward_ios, size: 18),
],
),
),
onPressed: () {
BaseNavigator.push(context, const ScriptsView());
},
),
),
),
],
);
}
}

View File

@@ -0,0 +1,234 @@
part of 'overwrite.dart';
class _StandardContent extends ConsumerStatefulWidget {
const _StandardContent();
@override
ConsumerState createState() => _StandardContentState();
}
class _StandardContentState extends ConsumerState<_StandardContent> {
final _key = utils.id;
late int _profileId;
Future<void> _handleAddOrUpdate([Rule? rule]) async {
final res = await globalState.showCommonDialog<Rule>(
child: AddOrEditRuleDialog(rule: rule),
);
if (res == null) {
return;
}
ref.read(profileAddedRulesProvider(_profileId).notifier).put(res);
}
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(profileAddedRulesProvider(_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(profileAddedRulesProvider(_profileId).notifier)
.delAll(selectedRules.cast<int>());
ref.read(selectedItemsProvider(_key).notifier).value = {};
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_profileId = ProfileIdProvider.of(context)!.profileId;
}
void _handleToEditGlobalAddedRules() {
BaseNavigator.push(context, _EditGlobalAddedRules(_profileId));
}
@override
Widget build(BuildContext context) {
_profileId = ProfileIdProvider.of(context)!.profileId;
final addedRules =
ref.watch(profileAddedRulesProvider(_profileId)).value ?? [];
final selectedRules = ref.watch(selectedItemsProvider(_key));
return CommonPopScope(
onPop: (_) {
if (selectedRules.isNotEmpty) {
ref.read(selectedItemsProvider(_key).notifier).value = {};
return false;
}
Navigator.of(context).pop();
return false;
},
child: SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
SliverToBoxAdapter(
child: Column(
children: [
InfoHeader(
info: Info(label: appLocalizations.addedRules),
actions: [
if (selectedRules.isNotEmpty) ...[
CommonMinIconButtonTheme(
child: IconButton.filledTonal(
onPressed: () {
_handleDelete();
},
icon: Icon(Icons.delete),
),
),
SizedBox(width: 8),
],
CommonMinFilledButtonTheme(
child: selectedRules.isNotEmpty
? FilledButton(
onPressed: () {
_handleSelectAll();
},
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
_handleAddOrUpdate();
},
child: Text(appLocalizations.add),
),
),
],
),
],
),
),
SliverToBoxAdapter(child: SizedBox(height: 8)),
Consumer(
builder: (_, ref, _) {
return SliverReorderableList(
itemCount: addedRules.length,
itemBuilder: (_, index) {
final rule = addedRules[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);
},
),
);
},
onReorder: ref
.read(profileAddedRulesProvider(_profileId).notifier)
.order,
);
},
),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: CommonCard(
radius: 18,
onPressed: _handleToEditGlobalAddedRules,
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(
appLocalizations.controlGlobalAddedRules,
style: context.textTheme.bodyLarge,
),
),
Icon(Icons.arrow_forward_ios, size: 18),
],
),
),
),
),
),
],
),
);
}
}
class _EditGlobalAddedRules extends ConsumerWidget {
final int profileId;
const _EditGlobalAddedRules(this.profileId);
void _handleChange(WidgetRef ref, int profileId, bool status, int ruleId) {
if (status) {
ref.read(profileDisabledRuleIdsProvider(profileId).notifier).put(ruleId);
} else {
ref.read(profileDisabledRuleIdsProvider(profileId).notifier).del(ruleId);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final disabledRuleIds =
ref.watch(profileDisabledRuleIdsProvider(profileId)).value ?? [];
final rules = ref.watch(globalRulesProvider).value ?? [];
return BaseScaffold(
title: appLocalizations.editGlobalRules,
body: rules.isEmpty
? NullStatus(
label: appLocalizations.nullTip(appLocalizations.rule),
illustration: RuleEmptyIllustration(),
)
: ListView.builder(
padding: EdgeInsets.all(16),
itemBuilder: (context, index) {
final rule = rules[index];
return RuleStatusItem(
status: !disabledRuleIds.contains(rule.id),
rule: rule,
onChange: (status) {
_handleChange(ref, profileId, !status, rule.id);
},
);
},
itemCount: rules.length,
),
);
}
}

View File

@@ -29,3 +29,39 @@ class _MoreActionButton extends StatelessWidget {
);
}
}
class ProfileIdProvider extends InheritedWidget {
final int profileId;
const ProfileIdProvider({
super.key,
required this.profileId,
required super.child,
});
static ProfileIdProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ProfileIdProvider>();
}
@override
bool updateShouldNotify(ProfileIdProvider oldWidget) =>
profileId != oldWidget.profileId;
}
class ProxyGroupProvider extends InheritedWidget {
final ProxyGroup proxyGroup;
const ProxyGroupProvider({
super.key,
required this.proxyGroup,
required super.child,
});
static ProxyGroupProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ProxyGroupProvider>();
}
@override
bool updateShouldNotify(ProxyGroupProvider oldWidget) =>
proxyGroup != oldWidget.proxyGroup;
}

View File

@@ -220,7 +220,7 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(top: 4),
padding: EdgeInsets.only(top: 6),
child: Container(
alignment: Alignment.center,
height: handleSize.height,