This commit is contained in:
chen08209
2026-03-05 17:29:05 +08:00
parent 50e0ae721e
commit bc552b63bb
10 changed files with 265 additions and 108 deletions

View File

@@ -6,16 +6,15 @@ class CommonTheme {
final Map<String, Color> _colorMap;
final double textScaleFactor;
CommonTheme.of(
this.context,
this.textScaleFactor,
) : _colorMap = {};
CommonTheme.of(this.context, this.textScaleFactor) : _colorMap = {};
Color get darkenSecondaryContainer {
return _colorMap.updateCacheValue(
'darkenSecondaryContainer',
() => context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1),
() => context.colorScheme.secondaryContainer.blendDarken(
context,
factor: 0.1,
),
);
}
@@ -31,16 +30,20 @@ class CommonTheme {
Color get darken2SecondaryContainer {
return _colorMap.updateCacheValue(
'darken2SecondaryContainer',
() => context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.2),
() => context.colorScheme.secondaryContainer.blendDarken(
context,
factor: 0.2,
),
);
}
Color get darken3PrimaryContainer {
return _colorMap.updateCacheValue(
'darken3PrimaryContainer',
() => context.colorScheme.primaryContainer
.blendDarken(context, factor: 0.3),
() => context.colorScheme.primaryContainer.blendDarken(
context,
factor: 0.3,
),
);
}
}

View File

@@ -430,3 +430,5 @@ enum LoadingTag { profiles, backup_restore, access, proxies }
enum CoreStatus { connecting, connected, disconnected }
enum RuleScene { added, disabled, custom }
enum ItemPosition { start, middle, end, startAndEnd }

View File

@@ -347,6 +347,14 @@ class ProfileDisabledRuleIds extends _$ProfileDisabledRuleIds
.watch();
}
@override
bool updateShouldNotify(
AsyncValue<List<int>> previous,
AsyncValue<List<int>> next,
) {
return !intListEquality.equals(previous.value, next.value);
}
void _put(int ruleId) {
var newList = List<int>.from(value);
final index = newList.indexWhere((item) => item == ruleId);

View File

@@ -792,7 +792,7 @@ final class ProfileDisabledRuleIdsProvider
}
String _$profileDisabledRuleIdsHash() =>
r'22d6e68bcee55b42fbb909e7f66e5c7095935224';
r'5093cc1d77ec69a2c1db6efa86a3f5916475d4f0';
final class ProfileDisabledRuleIdsFamily extends $Family
with

View File

@@ -215,54 +215,148 @@ class _EditCustomProxyGroupView extends ConsumerStatefulWidget {
class _EditCustomProxyGroupViewState
extends ConsumerState<_EditCustomProxyGroupView> {
Widget _buildItem({required Widget title, Widget? trailing}) {
return CommonInputListItem(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 16,
children: [
title,
if (trailing != null)
Flexible(
child: Container(
alignment: Alignment.centerRight,
height: globalState.measure.bodyLargeHeight + 6,
child: trailing,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Container(
return SizedBox(
height: appController.viewSize.height * 0.65,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 24),
children: [
generateSectionV3(
title: '通用',
items: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 16,
children: [
Text('名称'),
Flexible(
child: 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('类型')),
_buildItem(title: Text('图标')),
_buildItem(
title: Text('从列表中隐藏'),
trailing: Switch(value: false, onChanged: (_) {}),
),
_buildItem(
title: Text('禁用UDP'),
trailing: Switch(value: false, onChanged: (_) {}),
),
Text('类型'),
Text('图标'),
Text('从列表中隐藏'),
Text('禁用udp'),
],
),
generateSectionV3(
title: '节点',
items: [
Text('手动选择代理'),
Text('包括所有代理'),
Text('包括所有远程代理集'),
Text('正则表达式过滤器'),
Text('排除类型'),
Text('预期状态'),
_buildItem(title: Text('选择代理')),
_buildItem(title: Text('选择代理')),
_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: [Text('测试链接'), Text('最大失败时间'), Text('懒加载'), Text('测试间隔')],
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('删除'))],
),
generateSectionV3(title: '操作', items: [Text('删除')]),
],
),
);

View File

@@ -1,5 +1,6 @@
// ignore_for_file: deprecated_member_use
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/controller.dart';
import 'package:fl_clash/core/controller.dart';

View File

@@ -449,19 +449,26 @@ class _ReorderableProfilesSheetState extends State<ReorderableProfilesSheet> {
}
Widget _buildItem(int index, [bool isDecorator = false]) {
final isLast = index == profiles.length - 1;
final isFirst = index == 0;
ItemPosition position = ItemPosition.middle;
if (profiles.length == 1) {
position = ItemPosition.startAndEnd;
} else if (index == profiles.length - 1) {
position = ItemPosition.end;
} else if (index == 0) {
position = ItemPosition.start;
}
final profile = profiles[index];
return CommonInputListItem(
return ItemPositionProvider(
key: Key(profile.id.toString()),
trailing: ReorderableDelayedDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
position: position,
child: CommonInputListItem(
trailing: ReorderableDelayedDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
title: Text(profile.realLabel),
isDecorator: isDecorator,
),
title: Text(profile.realLabel),
isFirst: isFirst,
isLast: isLast,
isDecorator: isDecorator,
);
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
class CommonScaffoldBackActionProvider extends InheritedWidget {
@@ -39,3 +40,21 @@ class CommonScaffoldFabExtendedProvider extends InheritedWidget {
bool updateShouldNotify(CommonScaffoldFabExtendedProvider oldWidget) =>
isExtended != oldWidget.isExtended;
}
class ItemPositionProvider extends InheritedWidget {
final ItemPosition position;
const ItemPositionProvider({
super.key,
required this.position,
required super.child,
});
static ItemPositionProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ItemPositionProvider>();
}
@override
bool updateShouldNotify(ItemPositionProvider oldWidget) =>
position != oldWidget.position;
}

View File

@@ -1,8 +1,10 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/inherited.dart';
import 'package:fl_clash/widgets/null_status.dart';
import 'package:fl_clash/widgets/pop_scope.dart';
import 'package:fl_clash/widgets/scaffold.dart';
@@ -324,30 +326,37 @@ class _ListInputPageState extends ConsumerState<ListInputPage> {
required bool isEditing,
isDecorator = false,
}) {
final isFirst = index == 0;
final isLast = index == totalLength - 1;
ItemPosition position = ItemPosition.middle;
if (totalLength == 1) {
position = ItemPosition.startAndEnd;
} else if (index == totalLength - 1) {
position = ItemPosition.end;
} else if (index == 0) {
position = ItemPosition.start;
}
return ReorderableDelayedDragStartListener(
key: ValueKey(value),
index: index,
child: CommonSelectedInputListItem(
isDecorator: isDecorator,
isLast: isLast,
isFirst: isFirst,
title: widget.titleBuilder(value),
isSelected: isSelected,
isEditing: isEditing,
onSelected: () {
_handleSelected(value);
},
onPressed: () {
_handleAddOrEdit(value);
},
leading: widget.leadingBuilder != null
? widget.leadingBuilder!(value)
: null,
subtitle: widget.subtitleBuilder != null
? widget.subtitleBuilder!(value)
: null,
child: ItemPositionProvider(
position: position,
child: CommonSelectedInputListItem(
isDecorator: isDecorator,
title: widget.titleBuilder(value),
isSelected: isSelected,
isEditing: isEditing,
onSelected: () {
_handleSelected(value);
},
onPressed: () {
_handleAddOrEdit(value);
},
leading: widget.leadingBuilder != null
? widget.leadingBuilder!(value)
: null,
subtitle: widget.subtitleBuilder != null
? widget.subtitleBuilder!(value)
: null,
),
),
);
}
@@ -577,30 +586,37 @@ class _MapInputPageState extends ConsumerState<MapInputPage> {
required bool isEditing,
isDecorator = false,
}) {
final isFirst = index == 0;
final isLast = index == totalLength - 1;
ItemPosition position = ItemPosition.middle;
if (totalLength == 1) {
position = ItemPosition.startAndEnd;
} else if (index == totalLength - 1) {
position = ItemPosition.end;
} else if (index == 0) {
position = ItemPosition.start;
}
return ReorderableDelayedDragStartListener(
key: ValueKey(value),
index: index,
child: CommonSelectedInputListItem(
isDecorator: isDecorator,
isLast: isLast,
isFirst: isFirst,
title: widget.titleBuilder(value),
leading: widget.leadingBuilder != null
? widget.leadingBuilder!(value)
: null,
subtitle: widget.subtitleBuilder != null
? widget.subtitleBuilder!(value)
: null,
isSelected: isSelected,
isEditing: isEditing,
onSelected: () {
_handleSelected(value);
},
onPressed: () {
_handleAddOrEdit(value);
},
child: ItemPositionProvider(
position: position,
child: CommonSelectedInputListItem(
isDecorator: isDecorator,
title: widget.titleBuilder(value),
leading: widget.leadingBuilder != null
? widget.leadingBuilder!(value)
: null,
subtitle: widget.subtitleBuilder != null
? widget.subtitleBuilder!(value)
: null,
isSelected: isSelected,
isEditing: isEditing,
onSelected: () {
_handleSelected(value);
},
onPressed: () {
_handleAddOrEdit(value);
},
),
),
);
}

View File

@@ -3,6 +3,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/controller.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/inherited.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -545,12 +546,19 @@ Widget generateSectionV3({
String? title,
required Iterable<Widget> items,
List<Widget>? actions,
bool separated = true,
}) {
final genItems = items.mapIndexed<Widget>((index, item) {
final isFirst = index == 0;
final isLast = index == items.length - 1;
return CommonInputListItem(title: item, isFirst: isFirst, isLast: isLast);
if (items.length == 1) {
return ItemPositionProvider(
position: ItemPosition.startAndEnd,
child: item,
);
} else if (index == 0) {
return ItemPositionProvider(position: ItemPosition.start, child: item);
} else if (index == items.length - 1) {
return ItemPositionProvider(position: ItemPosition.end, child: item);
}
return item;
});
return Column(
children: [
@@ -643,8 +651,6 @@ class CommonSelectedListItem extends StatelessWidget {
class CommonInputListItem extends StatelessWidget {
final bool isDecorator;
final bool isFirst;
final bool isLast;
final Widget? title;
final Widget? subtitle;
final Widget? leading;
@@ -655,8 +661,6 @@ class CommonInputListItem extends StatelessWidget {
const CommonInputListItem({
super.key,
this.isDecorator = false,
this.isFirst = false,
this.isLast = false,
this.title,
this.leading,
this.trailing,
@@ -667,6 +671,15 @@ class CommonInputListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final position = ItemPositionProvider.of(context)?.position;
final isStart = [
ItemPosition.start,
ItemPosition.startAndEnd,
].contains(position);
final isEnd = [
ItemPosition.end,
ItemPosition.startAndEnd,
].contains(position);
return Container(
clipBehavior: Clip.hardEdge,
decoration: ShapeDecoration(
@@ -674,8 +687,8 @@ class CommonInputListItem extends StatelessWidget {
? LinearBorder.none
: RoundedSuperellipseBorder(
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(24) : Radius.zero,
bottom: isLast ? Radius.circular(24) : Radius.zero,
top: isStart ? Radius.circular(24) : Radius.zero,
bottom: isEnd ? Radius.circular(24) : Radius.zero,
),
),
),
@@ -697,7 +710,7 @@ class CommonInputListItem extends StatelessWidget {
trailing: trailing,
),
),
if (isDecorator != true && !isLast)
if (isDecorator != true && !isEnd)
Divider(height: 0, indent: 14, endIndent: 14),
],
),
@@ -713,8 +726,6 @@ class CommonSelectedInputListItem extends StatelessWidget {
final Widget? subtitle;
final VoidCallback onSelected;
final VoidCallback onPressed;
final bool isFirst;
final bool isLast;
final bool isDecorator;
final Widget? leading;
@@ -725,8 +736,6 @@ class CommonSelectedInputListItem extends StatelessWidget {
this.isEditing = false,
required this.title,
required this.onPressed,
this.isFirst = false,
this.isLast = false,
this.isDecorator = false,
this.subtitle,
this.leading,
@@ -738,8 +747,6 @@ class CommonSelectedInputListItem extends StatelessWidget {
title: title,
isDecorator: isDecorator,
isSelected: isSelected,
isFirst: isFirst,
isLast: isLast,
leading: leading,
onPressed: isDecorator
? null