feat(profiles): manage external resources from the profile details

Changelog: Profile-detail resources and include-all in custom groups
This commit is contained in:
chen08209
2026-10-16 20:56:11 +08:00
parent 2ea52fdfdc
commit 0bac50029a
53 changed files with 1716 additions and 2332 deletions
+4
View File
@@ -17,6 +17,10 @@ abstract final class TextInputLimits {
static const portRange = 11;
static const number = 10;
static const interval = number;
/// mihomo multiplies these hours into a Duration, which overflows past
/// about 2.5 million and panics its update ticker.
static const geoUpdateInterval = 6;
static const search = 256;
static const rule = 1024;
static const filter = 1024;
+1 -1
View File
@@ -45,7 +45,7 @@ class ProxyGroupsDao extends DatabaseAccessor<Database>
return stmt.map((item) => item.toProxyGroup());
}
Selectable<int> count(int profileId) {
Selectable<int> profileGroupsCount(int profileId) {
final stmt = proxyGroups.select();
stmt.where((row) => row.profileId.equals(profileId));
return stmt.count;
+38 -39
View File
@@ -88,7 +88,7 @@ enum EditorSchema {
ntp(_ntp, 'ntp'),
sniffer(_sniffer, 'sniffer'),
proxy(_proxy),
proxyGroup(_proxyGroup, 'proxy-groups'),
proxyGroup(_customProxyGroup, 'proxy-groups'),
rules(_rules),
shareLinks(_str),
provider(_providerFile);
@@ -1282,51 +1282,50 @@ const _healthCheck = YamlSchema.map({
'expected-status': _str,
});
const _groupFilters = {
'include-all': _bool,
'include-all-proxies': _bool,
'include-all-providers': _bool,
const _proxyGroupFields = {
'name': _str,
'type': YamlSchema.scalar(['select', 'url-test', 'fallback', 'load-balance']),
'proxies': YamlSchema.list(_policy),
'use': YamlSchema.list(YamlSchema.of(YamlScalar.proxyProvider)),
'use-position': YamlSchema.dict(_str),
'url': YamlSchema.scalar(['https://www.gstatic.com/generate_204']),
'interval': _str,
'lazy': _bool,
'timeout': _str,
'max-failed-times': _str,
'empty-fallback': _policy,
'expected-status': _str,
'filter': _str,
'exclude-filter': _str,
'exclude-type': _str,
'disable-udp': _bool,
'hidden': _bool,
'icon': _str,
};
const _proxyGroup = YamlSchema.map(
{
'name': _str,
'type': YamlSchema.scalar([
'select',
'url-test',
'fallback',
'load-balance',
const _proxyGroupVariants = {
'select': {'default-selected': _policy},
'url-test': {'tolerance': _str},
'load-balance': {
'strategy': YamlSchema.scalar([
'consistent-hashing',
'round-robin',
'sticky-sessions',
]),
'proxies': YamlSchema.list(_policy),
'use': YamlSchema.list(YamlSchema.of(YamlScalar.proxyProvider)),
'use-position': YamlSchema.dict(_str),
'url': YamlSchema.scalar(['https://www.gstatic.com/generate_204']),
'interval': _str,
'lazy': _bool,
'timeout': _str,
'max-failed-times': _str,
'empty-fallback': _policy,
'expected-status': _str,
..._groupFilters,
'disable-udp': _bool,
'hidden': _bool,
'icon': _str,
},
variants: {
'select': {'default-selected': _policy},
'url-test': {'tolerance': _str},
'load-balance': {
'strategy': YamlSchema.scalar([
'consistent-hashing',
'round-robin',
'sticky-sessions',
]),
'hash-key': YamlSchema.scalar(['in-user']),
},
'hash-key': YamlSchema.scalar(['in-user']),
},
};
const _proxyGroup = YamlSchema.map({
..._proxyGroupFields,
'include-all': _bool,
'include-all-proxies': _bool,
'include-all-providers': _bool,
}, variants: _proxyGroupVariants);
const _customProxyGroup = YamlSchema.map(
_proxyGroupFields,
variants: _proxyGroupVariants,
);
const _providerType = YamlSchema.scalar(['http', 'file', 'inline']);
+6 -5
View File
@@ -1,7 +1,8 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/ui/ui.dart';
import 'package:material_ui/material_ui.dart';
import 'form_row.dart';
@@ -153,13 +154,13 @@ class _Banner extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
GlyphIcon(glyph, size: 20.ap, color: foregroundColor),
GlyphIcon(glyph, size: 20.ap(context), color: foregroundColor),
Expanded(
child: Text(
message,
style: context.textTheme.bodyMedium?.copyWith(
color: foregroundColor,
),
style: AppTextStyle.subheadline
.of(context)
.copyWith(color: foregroundColor),
),
),
],
+1 -2
View File
@@ -315,8 +315,7 @@ class _RuleFormState extends State<RuleForm> {
return;
}
Navigator.of(context).pop(
Rule(
id: widget.rule?.id ?? snowflake.id,
(widget.rule ?? Rule(id: snowflake.id)).copyWith(
ruleAction: _ruleAction,
content: _contentController.text,
ruleTarget: _ruleTarget,
+25 -5
View File
@@ -7717,13 +7717,33 @@ class AppLocalizations {
);
}
/// `Dropped include-all from the proxy groups {groups}; add the proxies or proxy providers they use`
String customImportDroppedIncludeAll(Object groups) {
/// `Auto-include`
String get autoInclude {
return Intl.message(
'Dropped include-all from the proxy groups $groups; add the proxies or proxy providers they use',
name: 'customImportDroppedIncludeAll',
'Auto-include',
name: 'autoInclude',
desc: '',
args: [groups],
args: [],
);
}
/// `All referenced proxies`
String get allReferencedProxies {
return Intl.message(
'All referenced proxies',
name: 'allReferencedProxies',
desc: '',
args: [],
);
}
/// `All referenced proxy providers`
String get allReferencedProxyProviders {
return Intl.message(
'All referenced proxy providers',
name: 'allReferencedProxyProviders',
desc: '',
args: [],
);
}
}
+10 -24
View File
@@ -61,6 +61,10 @@ const privateRouteAddress = [
'ff00::/8',
];
Object? _readIncludeAll(Map<dynamic, dynamic> json, String key) {
return json['include-all'] == true ? true : json[key];
}
@freezed
abstract class ProxyGroup with _$ProxyGroup {
const factory ProxyGroup({
@@ -86,9 +90,10 @@ abstract class ProxyGroup with _$ProxyGroup {
@JsonKey(name: 'hash-key') String? hashKey,
@JsonKey(name: 'default-selected') String? defaultSelected,
@JsonKey(name: 'empty-fallback') String? emptyFallback,
@JsonKey(name: 'include-all') bool? includeAll,
@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,
@JsonKey(name: 'include-all-providers') bool? includeAllProviders,
@JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll)
bool? includeAllProxies,
@JsonKey(name: 'include-all-providers', readValue: _readIncludeAll)
bool? includeAllProviders,
bool? hidden,
String? icon,
String? order,
@@ -123,10 +128,8 @@ extension ProxyGroupExt on ProxyGroup {
if (excludeFilter?.isNotEmpty == true) 'exclude-filter': excludeFilter,
if (excludeType?.isNotEmpty == true) 'exclude-type': excludeType,
if (expectedStatus?.isNotEmpty == true) 'expected-status': expectedStatus,
if (includeAll != null) 'include-all': includeAll,
if (includeAllProxies != null) 'include-all-proxies': includeAllProxies,
if (includeAllProviders != null)
'include-all-providers': includeAllProviders,
if (includeAllProxies == true) 'include-all-proxies': true,
if (includeAllProviders == true) 'include-all-providers': true,
if (hidden != null) 'hidden': hidden,
if (icon?.isNotEmpty == true) 'icon': icon,
if (type == GroupType.URLTest && tolerance != null)
@@ -851,22 +854,6 @@ extension CustomProfileDataExt on CustomProfileData {
namedProxies.contains(name);
}
@freezed
abstract class RuleProvider with _$RuleProvider {
const factory RuleProvider({required String name}) = _RuleProvider;
factory RuleProvider.fromJson(Map<String, Object?> json) =>
_$RuleProviderFromJson(json);
}
@freezed
abstract class ProxyProvider with _$ProxyProvider {
const factory ProxyProvider({required String name}) = _ProxyProvider;
factory ProxyProvider.fromJson(Map<String, Object?> json) =>
_$ProxyProviderFromJson(json);
}
@freezed
abstract class Sniffer with _$Sniffer {
const factory Sniffer({
@@ -1869,7 +1856,6 @@ abstract class ClashConfig with _$ClashConfig {
@JsonKey(name: 'sub-rules', fromJson: _genList)
@Default([])
List<String> subRules,
@Default({}) Map<String, String> proxyTypeMap,
}) = _ClashConfig;
factory ClashConfig.fromJson(Map<String, Object?> json) =>
+42 -594
View File
@@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProxyGroup {
int? get profileId;@JsonKey(fromJson: Snowflake.buildId) int get id; String get name; GroupType get type; List<String>? get proxies; List<String>? get use;@JsonKey(name: 'use-position') Map<String, int>? get usePosition; int? get interval; bool? get lazy;@JsonKey(name: 'disable-udp') bool? get disableUDP; String? get url; int? get timeout;@JsonKey(name: 'max-failed-times') int? get maxFailedTimes; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'exclude-type') String? get excludeType;@JsonKey(name: 'expected-status') String? get expectedStatus; int? get tolerance; LoadBalanceStrategy? get strategy;@JsonKey(name: 'hash-key') String? get hashKey;@JsonKey(name: 'default-selected') String? get defaultSelected;@JsonKey(name: 'empty-fallback') String? get emptyFallback;@JsonKey(name: 'include-all') bool? get includeAll;@JsonKey(name: 'include-all-proxies') bool? get includeAllProxies;@JsonKey(name: 'include-all-providers') bool? get includeAllProviders; bool? get hidden; String? get icon; String? get order;/// Where the members list shows the filters among the proxies, which no
int? get profileId;@JsonKey(fromJson: Snowflake.buildId) int get id; String get name; GroupType get type; List<String>? get proxies; List<String>? get use;@JsonKey(name: 'use-position') Map<String, int>? get usePosition; int? get interval; bool? get lazy;@JsonKey(name: 'disable-udp') bool? get disableUDP; String? get url; int? get timeout;@JsonKey(name: 'max-failed-times') int? get maxFailedTimes; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'exclude-type') String? get excludeType;@JsonKey(name: 'expected-status') String? get expectedStatus; int? get tolerance; LoadBalanceStrategy? get strategy;@JsonKey(name: 'hash-key') String? get hashKey;@JsonKey(name: 'default-selected') String? get defaultSelected;@JsonKey(name: 'empty-fallback') String? get emptyFallback;@JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? get includeAllProxies;@JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? get includeAllProviders; bool? get hidden; String? get icon; String? get order;/// Where the members list shows the filters among the proxies, which no
/// config carries, as `ProxyGroupMembersSliver` writes it.
List<String>? get memberLayout;
/// Create a copy of ProxyGroup
@@ -32,20 +32,20 @@ $ProxyGroupCopyWith<ProxyGroup> get copyWith => _$ProxyGroupCopyWithImpl<ProxyGr
@override
bool operator ==(Object other) {
final _this = this as ProxyGroup;
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyGroup&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.name, _this.name) || other.name == _this.name)&&(identical(other.type, _this.type) || other.type == _this.type)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.use, _this.use)&&const DeepCollectionEquality().equals(other.usePosition, _this.usePosition)&&(identical(other.interval, _this.interval) || other.interval == _this.interval)&&(identical(other.lazy, _this.lazy) || other.lazy == _this.lazy)&&(identical(other.disableUDP, _this.disableUDP) || other.disableUDP == _this.disableUDP)&&(identical(other.url, _this.url) || other.url == _this.url)&&(identical(other.timeout, _this.timeout) || other.timeout == _this.timeout)&&(identical(other.maxFailedTimes, _this.maxFailedTimes) || other.maxFailedTimes == _this.maxFailedTimes)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.excludeType, _this.excludeType) || other.excludeType == _this.excludeType)&&(identical(other.expectedStatus, _this.expectedStatus) || other.expectedStatus == _this.expectedStatus)&&(identical(other.tolerance, _this.tolerance) || other.tolerance == _this.tolerance)&&(identical(other.strategy, _this.strategy) || other.strategy == _this.strategy)&&(identical(other.hashKey, _this.hashKey) || other.hashKey == _this.hashKey)&&(identical(other.defaultSelected, _this.defaultSelected) || other.defaultSelected == _this.defaultSelected)&&(identical(other.emptyFallback, _this.emptyFallback) || other.emptyFallback == _this.emptyFallback)&&(identical(other.includeAll, _this.includeAll) || other.includeAll == _this.includeAll)&&(identical(other.includeAllProxies, _this.includeAllProxies) || other.includeAllProxies == _this.includeAllProxies)&&(identical(other.includeAllProviders, _this.includeAllProviders) || other.includeAllProviders == _this.includeAllProviders)&&(identical(other.hidden, _this.hidden) || other.hidden == _this.hidden)&&(identical(other.icon, _this.icon) || other.icon == _this.icon)&&(identical(other.order, _this.order) || other.order == _this.order)&&const DeepCollectionEquality().equals(other.memberLayout, _this.memberLayout));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyGroup&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.name, _this.name) || other.name == _this.name)&&(identical(other.type, _this.type) || other.type == _this.type)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.use, _this.use)&&const DeepCollectionEquality().equals(other.usePosition, _this.usePosition)&&(identical(other.interval, _this.interval) || other.interval == _this.interval)&&(identical(other.lazy, _this.lazy) || other.lazy == _this.lazy)&&(identical(other.disableUDP, _this.disableUDP) || other.disableUDP == _this.disableUDP)&&(identical(other.url, _this.url) || other.url == _this.url)&&(identical(other.timeout, _this.timeout) || other.timeout == _this.timeout)&&(identical(other.maxFailedTimes, _this.maxFailedTimes) || other.maxFailedTimes == _this.maxFailedTimes)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.excludeType, _this.excludeType) || other.excludeType == _this.excludeType)&&(identical(other.expectedStatus, _this.expectedStatus) || other.expectedStatus == _this.expectedStatus)&&(identical(other.tolerance, _this.tolerance) || other.tolerance == _this.tolerance)&&(identical(other.strategy, _this.strategy) || other.strategy == _this.strategy)&&(identical(other.hashKey, _this.hashKey) || other.hashKey == _this.hashKey)&&(identical(other.defaultSelected, _this.defaultSelected) || other.defaultSelected == _this.defaultSelected)&&(identical(other.emptyFallback, _this.emptyFallback) || other.emptyFallback == _this.emptyFallback)&&(identical(other.includeAllProxies, _this.includeAllProxies) || other.includeAllProxies == _this.includeAllProxies)&&(identical(other.includeAllProviders, _this.includeAllProviders) || other.includeAllProviders == _this.includeAllProviders)&&(identical(other.hidden, _this.hidden) || other.hidden == _this.hidden)&&(identical(other.icon, _this.icon) || other.icon == _this.icon)&&(identical(other.order, _this.order) || other.order == _this.order)&&const DeepCollectionEquality().equals(other.memberLayout, _this.memberLayout));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as ProxyGroup;
return Object.hashAll([runtimeType,_this.profileId,_this.id,_this.name,_this.type,const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.use),const DeepCollectionEquality().hash(_this.usePosition),_this.interval,_this.lazy,_this.disableUDP,_this.url,_this.timeout,_this.maxFailedTimes,_this.filter,_this.excludeFilter,_this.excludeType,_this.expectedStatus,_this.tolerance,_this.strategy,_this.hashKey,_this.defaultSelected,_this.emptyFallback,_this.includeAll,_this.includeAllProxies,_this.includeAllProviders,_this.hidden,_this.icon,_this.order,const DeepCollectionEquality().hash(_this.memberLayout)]);
return Object.hashAll([runtimeType,_this.profileId,_this.id,_this.name,_this.type,const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.use),const DeepCollectionEquality().hash(_this.usePosition),_this.interval,_this.lazy,_this.disableUDP,_this.url,_this.timeout,_this.maxFailedTimes,_this.filter,_this.excludeFilter,_this.excludeType,_this.expectedStatus,_this.tolerance,_this.strategy,_this.hashKey,_this.defaultSelected,_this.emptyFallback,_this.includeAllProxies,_this.includeAllProviders,_this.hidden,_this.icon,_this.order,const DeepCollectionEquality().hash(_this.memberLayout)]);
}
@override
String toString() {
final _this = this as ProxyGroup;
return 'ProxyGroup(profileId: ${_this.profileId}, id: ${_this.id}, name: ${_this.name}, type: ${_this.type}, proxies: ${_this.proxies}, use: ${_this.use}, usePosition: ${_this.usePosition}, interval: ${_this.interval}, lazy: ${_this.lazy}, disableUDP: ${_this.disableUDP}, url: ${_this.url}, timeout: ${_this.timeout}, maxFailedTimes: ${_this.maxFailedTimes}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, excludeType: ${_this.excludeType}, expectedStatus: ${_this.expectedStatus}, tolerance: ${_this.tolerance}, strategy: ${_this.strategy}, hashKey: ${_this.hashKey}, defaultSelected: ${_this.defaultSelected}, emptyFallback: ${_this.emptyFallback}, includeAll: ${_this.includeAll}, includeAllProxies: ${_this.includeAllProxies}, includeAllProviders: ${_this.includeAllProviders}, hidden: ${_this.hidden}, icon: ${_this.icon}, order: ${_this.order}, memberLayout: ${_this.memberLayout})';
return 'ProxyGroup(profileId: ${_this.profileId}, id: ${_this.id}, name: ${_this.name}, type: ${_this.type}, proxies: ${_this.proxies}, use: ${_this.use}, usePosition: ${_this.usePosition}, interval: ${_this.interval}, lazy: ${_this.lazy}, disableUDP: ${_this.disableUDP}, url: ${_this.url}, timeout: ${_this.timeout}, maxFailedTimes: ${_this.maxFailedTimes}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, excludeType: ${_this.excludeType}, expectedStatus: ${_this.expectedStatus}, tolerance: ${_this.tolerance}, strategy: ${_this.strategy}, hashKey: ${_this.hashKey}, defaultSelected: ${_this.defaultSelected}, emptyFallback: ${_this.emptyFallback}, includeAllProxies: ${_this.includeAllProxies}, includeAllProviders: ${_this.includeAllProviders}, hidden: ${_this.hidden}, icon: ${_this.icon}, order: ${_this.order}, memberLayout: ${_this.memberLayout})';
}
@@ -56,7 +56,7 @@ abstract mixin class $ProxyGroupCopyWith<$Res> {
factory $ProxyGroupCopyWith(ProxyGroup value, $Res Function(ProxyGroup) _then) = _$ProxyGroupCopyWithImpl;
@useResult
$Res call({
int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout
int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? includeAllProxies,@JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout
});
@@ -73,7 +73,7 @@ class _$ProxyGroupCopyWithImpl<$Res>
/// Create a copy of ProxyGroup
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) {
return _then(ProxyGroup(
profileId: freezed == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable
as int?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
@@ -97,8 +97,7 @@ as int?,strategy: freezed == strategy ? _self.strategy : strategy // ignore: cas
as LoadBalanceStrategy?,hashKey: freezed == hashKey ? _self.hashKey : hashKey // ignore: cast_nullable_to_non_nullable
as String?,defaultSelected: freezed == defaultSelected ? _self.defaultSelected : defaultSelected // ignore: cast_nullable_to_non_nullable
as String?,emptyFallback: freezed == emptyFallback ? _self.emptyFallback : emptyFallback // ignore: cast_nullable_to_non_nullable
as String?,includeAll: freezed == includeAll ? _self.includeAll : includeAll // ignore: cast_nullable_to_non_nullable
as bool?,includeAllProxies: freezed == includeAllProxies ? _self.includeAllProxies : includeAllProxies // ignore: cast_nullable_to_non_nullable
as String?,includeAllProxies: freezed == includeAllProxies ? _self.includeAllProxies : includeAllProxies // ignore: cast_nullable_to_non_nullable
as bool?,includeAllProviders: freezed == includeAllProviders ? _self.includeAllProviders : includeAllProviders // ignore: cast_nullable_to_non_nullable
as bool?,hidden: freezed == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable
as bool?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
@@ -189,10 +188,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? includeAllProxies, @JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProxyGroup() when $default != null:
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
return orElse();
}
@@ -210,10 +209,10 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? includeAllProxies, @JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout) $default,) {final _that = this;
switch (_that) {
case _ProxyGroup():
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
throw StateError('Unexpected subclass');
}
@@ -230,10 +229,10 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? includeAllProxies, @JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout)? $default,) {final _that = this;
switch (_that) {
case _ProxyGroup() when $default != null:
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _:
return null;
}
@@ -245,7 +244,7 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th
@JsonSerializable()
class _ProxyGroup implements ProxyGroup {
const _ProxyGroup({this.profileId, @JsonKey(fromJson: Snowflake.buildId) required this.id, required this.name, required this.type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, this.interval, this.lazy, @JsonKey(name: 'disable-udp') this.disableUDP, this.url, this.timeout, @JsonKey(name: 'max-failed-times') this.maxFailedTimes, this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'exclude-type') this.excludeType, @JsonKey(name: 'expected-status') this.expectedStatus, this.tolerance, this.strategy, @JsonKey(name: 'hash-key') this.hashKey, @JsonKey(name: 'default-selected') this.defaultSelected, @JsonKey(name: 'empty-fallback') this.emptyFallback, @JsonKey(name: 'include-all') this.includeAll, @JsonKey(name: 'include-all-proxies') this.includeAllProxies, @JsonKey(name: 'include-all-providers') this.includeAllProviders, this.hidden, this.icon, this.order, List<String>? memberLayout}): _proxies = proxies,_use = use,_usePosition = usePosition,_memberLayout = memberLayout;
const _ProxyGroup({this.profileId, @JsonKey(fromJson: Snowflake.buildId) required this.id, required this.name, required this.type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? usePosition, this.interval, this.lazy, @JsonKey(name: 'disable-udp') this.disableUDP, this.url, this.timeout, @JsonKey(name: 'max-failed-times') this.maxFailedTimes, this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'exclude-type') this.excludeType, @JsonKey(name: 'expected-status') this.expectedStatus, this.tolerance, this.strategy, @JsonKey(name: 'hash-key') this.hashKey, @JsonKey(name: 'default-selected') this.defaultSelected, @JsonKey(name: 'empty-fallback') this.emptyFallback, @JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) this.includeAllProxies, @JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) this.includeAllProviders, this.hidden, this.icon, this.order, List<String>? memberLayout}): _proxies = proxies,_use = use,_usePosition = usePosition,_memberLayout = memberLayout;
factory _ProxyGroup.fromJson(Map<String, dynamic> json) => _$ProxyGroupFromJson(json);
@override final int? profileId;
@@ -294,9 +293,8 @@ class _ProxyGroup implements ProxyGroup {
@override@JsonKey(name: 'hash-key') final String? hashKey;
@override@JsonKey(name: 'default-selected') final String? defaultSelected;
@override@JsonKey(name: 'empty-fallback') final String? emptyFallback;
@override@JsonKey(name: 'include-all') final bool? includeAll;
@override@JsonKey(name: 'include-all-proxies') final bool? includeAllProxies;
@override@JsonKey(name: 'include-all-providers') final bool? includeAllProviders;
@override@JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) final bool? includeAllProxies;
@override@JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) final bool? includeAllProviders;
@override final bool? hidden;
@override final String? icon;
@override final String? order;
@@ -327,18 +325,18 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyGroup&&(identical(other.profileId, profileId) || other.profileId == profileId)&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.use, _use)&&const DeepCollectionEquality().equals(other.usePosition, _usePosition)&&(identical(other.interval, interval) || other.interval == interval)&&(identical(other.lazy, lazy) || other.lazy == lazy)&&(identical(other.disableUDP, disableUDP) || other.disableUDP == disableUDP)&&(identical(other.url, url) || other.url == url)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.maxFailedTimes, maxFailedTimes) || other.maxFailedTimes == maxFailedTimes)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.excludeType, excludeType) || other.excludeType == excludeType)&&(identical(other.expectedStatus, expectedStatus) || other.expectedStatus == expectedStatus)&&(identical(other.tolerance, tolerance) || other.tolerance == tolerance)&&(identical(other.strategy, strategy) || other.strategy == strategy)&&(identical(other.hashKey, hashKey) || other.hashKey == hashKey)&&(identical(other.defaultSelected, defaultSelected) || other.defaultSelected == defaultSelected)&&(identical(other.emptyFallback, emptyFallback) || other.emptyFallback == emptyFallback)&&(identical(other.includeAll, includeAll) || other.includeAll == includeAll)&&(identical(other.includeAllProxies, includeAllProxies) || other.includeAllProxies == includeAllProxies)&&(identical(other.includeAllProviders, includeAllProviders) || other.includeAllProviders == includeAllProviders)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.order, order) || other.order == order)&&const DeepCollectionEquality().equals(other.memberLayout, _memberLayout));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyGroup&&(identical(other.profileId, profileId) || other.profileId == profileId)&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.use, _use)&&const DeepCollectionEquality().equals(other.usePosition, _usePosition)&&(identical(other.interval, interval) || other.interval == interval)&&(identical(other.lazy, lazy) || other.lazy == lazy)&&(identical(other.disableUDP, disableUDP) || other.disableUDP == disableUDP)&&(identical(other.url, url) || other.url == url)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.maxFailedTimes, maxFailedTimes) || other.maxFailedTimes == maxFailedTimes)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.excludeType, excludeType) || other.excludeType == excludeType)&&(identical(other.expectedStatus, expectedStatus) || other.expectedStatus == expectedStatus)&&(identical(other.tolerance, tolerance) || other.tolerance == tolerance)&&(identical(other.strategy, strategy) || other.strategy == strategy)&&(identical(other.hashKey, hashKey) || other.hashKey == hashKey)&&(identical(other.defaultSelected, defaultSelected) || other.defaultSelected == defaultSelected)&&(identical(other.emptyFallback, emptyFallback) || other.emptyFallback == emptyFallback)&&(identical(other.includeAllProxies, includeAllProxies) || other.includeAllProxies == includeAllProxies)&&(identical(other.includeAllProviders, includeAllProviders) || other.includeAllProviders == includeAllProviders)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.order, order) || other.order == order)&&const DeepCollectionEquality().equals(other.memberLayout, _memberLayout));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hashAll([runtimeType,profileId,id,name,type,const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_use),const DeepCollectionEquality().hash(_usePosition),interval,lazy,disableUDP,url,timeout,maxFailedTimes,filter,excludeFilter,excludeType,expectedStatus,tolerance,strategy,hashKey,defaultSelected,emptyFallback,includeAll,includeAllProxies,includeAllProviders,hidden,icon,order,const DeepCollectionEquality().hash(_memberLayout)]);
return Object.hashAll([runtimeType,profileId,id,name,type,const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_use),const DeepCollectionEquality().hash(_usePosition),interval,lazy,disableUDP,url,timeout,maxFailedTimes,filter,excludeFilter,excludeType,expectedStatus,tolerance,strategy,hashKey,defaultSelected,emptyFallback,includeAllProxies,includeAllProviders,hidden,icon,order,const DeepCollectionEquality().hash(_memberLayout)]);
}
@override
String toString() {
return 'ProxyGroup(profileId: $profileId, id: $id, name: $name, type: $type, proxies: $proxies, use: $use, usePosition: $usePosition, interval: $interval, lazy: $lazy, disableUDP: $disableUDP, url: $url, timeout: $timeout, maxFailedTimes: $maxFailedTimes, filter: $filter, excludeFilter: $excludeFilter, excludeType: $excludeType, expectedStatus: $expectedStatus, tolerance: $tolerance, strategy: $strategy, hashKey: $hashKey, defaultSelected: $defaultSelected, emptyFallback: $emptyFallback, includeAll: $includeAll, includeAllProxies: $includeAllProxies, includeAllProviders: $includeAllProviders, hidden: $hidden, icon: $icon, order: $order, memberLayout: $memberLayout)';
return 'ProxyGroup(profileId: $profileId, id: $id, name: $name, type: $type, proxies: $proxies, use: $use, usePosition: $usePosition, interval: $interval, lazy: $lazy, disableUDP: $disableUDP, url: $url, timeout: $timeout, maxFailedTimes: $maxFailedTimes, filter: $filter, excludeFilter: $excludeFilter, excludeType: $excludeType, expectedStatus: $expectedStatus, tolerance: $tolerance, strategy: $strategy, hashKey: $hashKey, defaultSelected: $defaultSelected, emptyFallback: $emptyFallback, includeAllProxies: $includeAllProxies, includeAllProviders: $includeAllProviders, hidden: $hidden, icon: $icon, order: $order, memberLayout: $memberLayout)';
}
@@ -349,7 +347,7 @@ abstract mixin class _$ProxyGroupCopyWith<$Res> implements $ProxyGroupCopyWith<$
factory _$ProxyGroupCopyWith(_ProxyGroup value, $Res Function(_ProxyGroup) _then) = __$ProxyGroupCopyWithImpl;
@override @useResult
$Res call({
int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout
int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all-proxies', readValue: _readIncludeAll) bool? includeAllProxies,@JsonKey(name: 'include-all-providers', readValue: _readIncludeAll) bool? includeAllProviders, bool? hidden, String? icon, String? order, List<String>? memberLayout
});
@@ -366,7 +364,7 @@ class __$ProxyGroupCopyWithImpl<$Res>
/// Create a copy of ProxyGroup
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) {
return _then(_ProxyGroup(
profileId: freezed == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable
as int?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
@@ -390,8 +388,7 @@ as int?,strategy: freezed == strategy ? _self.strategy : strategy // ignore: cas
as LoadBalanceStrategy?,hashKey: freezed == hashKey ? _self.hashKey : hashKey // ignore: cast_nullable_to_non_nullable
as String?,defaultSelected: freezed == defaultSelected ? _self.defaultSelected : defaultSelected // ignore: cast_nullable_to_non_nullable
as String?,emptyFallback: freezed == emptyFallback ? _self.emptyFallback : emptyFallback // ignore: cast_nullable_to_non_nullable
as String?,includeAll: freezed == includeAll ? _self.includeAll : includeAll // ignore: cast_nullable_to_non_nullable
as bool?,includeAllProxies: freezed == includeAllProxies ? _self.includeAllProxies : includeAllProxies // ignore: cast_nullable_to_non_nullable
as String?,includeAllProxies: freezed == includeAllProxies ? _self.includeAllProxies : includeAllProxies // ignore: cast_nullable_to_non_nullable
as bool?,includeAllProviders: freezed == includeAllProviders ? _self.includeAllProviders : includeAllProviders // ignore: cast_nullable_to_non_nullable
as bool?,hidden: freezed == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable
as bool?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
@@ -3151,546 +3148,6 @@ as Map<String, String>,
}
/// @nodoc
mixin _$RuleProvider {
String get name;
/// Create a copy of RuleProvider
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RuleProviderCopyWith<RuleProvider> get copyWith => _$RuleProviderCopyWithImpl<RuleProvider>(this as RuleProvider, _$identity);
/// Serializes this RuleProvider to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
final _this = this as RuleProvider;
return identical(this, other) || (other.runtimeType == runtimeType&&other is RuleProvider&&(identical(other.name, _this.name) || other.name == _this.name));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as RuleProvider;
return Object.hash(runtimeType,_this.name);
}
@override
String toString() {
final _this = this as RuleProvider;
return 'RuleProvider(name: ${_this.name})';
}
}
/// @nodoc
abstract mixin class $RuleProviderCopyWith<$Res> {
factory $RuleProviderCopyWith(RuleProvider value, $Res Function(RuleProvider) _then) = _$RuleProviderCopyWithImpl;
@useResult
$Res call({
String name
});
}
/// @nodoc
class _$RuleProviderCopyWithImpl<$Res>
implements $RuleProviderCopyWith<$Res> {
_$RuleProviderCopyWithImpl(this._self, this._then);
final RuleProvider _self;
final $Res Function(RuleProvider) _then;
/// Create a copy of RuleProvider
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,}) {
return _then(RuleProvider(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [RuleProvider].
extension RuleProviderPatterns on RuleProvider {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RuleProvider value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RuleProvider() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RuleProvider value) $default,){
final _that = this;
switch (_that) {
case _RuleProvider():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RuleProvider value)? $default,){
final _that = this;
switch (_that) {
case _RuleProvider() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RuleProvider() when $default != null:
return $default(_that.name);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name) $default,) {final _that = this;
switch (_that) {
case _RuleProvider():
return $default(_that.name);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name)? $default,) {final _that = this;
switch (_that) {
case _RuleProvider() when $default != null:
return $default(_that.name);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _RuleProvider implements RuleProvider {
const _RuleProvider({required this.name});
factory _RuleProvider.fromJson(Map<String, dynamic> json) => _$RuleProviderFromJson(json);
@override final String name;
/// Create a copy of RuleProvider
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RuleProviderCopyWith<_RuleProvider> get copyWith => __$RuleProviderCopyWithImpl<_RuleProvider>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$RuleProviderToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RuleProvider&&(identical(other.name, name) || other.name == name));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hash(runtimeType,name);
}
@override
String toString() {
return 'RuleProvider(name: $name)';
}
}
/// @nodoc
abstract mixin class _$RuleProviderCopyWith<$Res> implements $RuleProviderCopyWith<$Res> {
factory _$RuleProviderCopyWith(_RuleProvider value, $Res Function(_RuleProvider) _then) = __$RuleProviderCopyWithImpl;
@override @useResult
$Res call({
String name
});
}
/// @nodoc
class __$RuleProviderCopyWithImpl<$Res>
implements _$RuleProviderCopyWith<$Res> {
__$RuleProviderCopyWithImpl(this._self, this._then);
final _RuleProvider _self;
final $Res Function(_RuleProvider) _then;
/// Create a copy of RuleProvider
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,}) {
return _then(_RuleProvider(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$ProxyProvider {
String get name;
/// Create a copy of ProxyProvider
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProxyProviderCopyWith<ProxyProvider> get copyWith => _$ProxyProviderCopyWithImpl<ProxyProvider>(this as ProxyProvider, _$identity);
/// Serializes this ProxyProvider to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
final _this = this as ProxyProvider;
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyProvider&&(identical(other.name, _this.name) || other.name == _this.name));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as ProxyProvider;
return Object.hash(runtimeType,_this.name);
}
@override
String toString() {
final _this = this as ProxyProvider;
return 'ProxyProvider(name: ${_this.name})';
}
}
/// @nodoc
abstract mixin class $ProxyProviderCopyWith<$Res> {
factory $ProxyProviderCopyWith(ProxyProvider value, $Res Function(ProxyProvider) _then) = _$ProxyProviderCopyWithImpl;
@useResult
$Res call({
String name
});
}
/// @nodoc
class _$ProxyProviderCopyWithImpl<$Res>
implements $ProxyProviderCopyWith<$Res> {
_$ProxyProviderCopyWithImpl(this._self, this._then);
final ProxyProvider _self;
final $Res Function(ProxyProvider) _then;
/// Create a copy of ProxyProvider
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,}) {
return _then(ProxyProvider(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [ProxyProvider].
extension ProxyProviderPatterns on ProxyProvider {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProxyProvider value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProxyProvider() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProxyProvider value) $default,){
final _that = this;
switch (_that) {
case _ProxyProvider():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProxyProvider value)? $default,){
final _that = this;
switch (_that) {
case _ProxyProvider() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProxyProvider() when $default != null:
return $default(_that.name);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name) $default,) {final _that = this;
switch (_that) {
case _ProxyProvider():
return $default(_that.name);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name)? $default,) {final _that = this;
switch (_that) {
case _ProxyProvider() when $default != null:
return $default(_that.name);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ProxyProvider implements ProxyProvider {
const _ProxyProvider({required this.name});
factory _ProxyProvider.fromJson(Map<String, dynamic> json) => _$ProxyProviderFromJson(json);
@override final String name;
/// Create a copy of ProxyProvider
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProxyProviderCopyWith<_ProxyProvider> get copyWith => __$ProxyProviderCopyWithImpl<_ProxyProvider>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ProxyProviderToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyProvider&&(identical(other.name, name) || other.name == name));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hash(runtimeType,name);
}
@override
String toString() {
return 'ProxyProvider(name: $name)';
}
}
/// @nodoc
abstract mixin class _$ProxyProviderCopyWith<$Res> implements $ProxyProviderCopyWith<$Res> {
factory _$ProxyProviderCopyWith(_ProxyProvider value, $Res Function(_ProxyProvider) _then) = __$ProxyProviderCopyWithImpl;
@override @useResult
$Res call({
String name
});
}
/// @nodoc
class __$ProxyProviderCopyWithImpl<$Res>
implements _$ProxyProviderCopyWith<$Res> {
__$ProxyProviderCopyWithImpl(this._self, this._then);
final _ProxyProvider _self;
final $Res Function(_ProxyProvider) _then;
/// Create a copy of ProxyProvider
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,}) {
return _then(_ProxyProvider(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$Sniffer {
@@ -7479,7 +6936,7 @@ as String?,
/// @nodoc
mixin _$ClashConfig {
@JsonKey(name: 'proxy-groups') List<ProxyGroup> get proxyGroups;@JsonKey(fromJson: _genRules) List<Rule> get rules; List<Proxy> get proxies;@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> get proxyProviders;@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> get ruleProviders;@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> get subRules; Map<String, String> get proxyTypeMap;
@JsonKey(name: 'proxy-groups') List<ProxyGroup> get proxyGroups;@JsonKey(fromJson: _genRules) List<Rule> get rules; List<Proxy> get proxies;@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> get proxyProviders;@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> get ruleProviders;@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> get subRules;
/// Create a copy of ClashConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -7493,20 +6950,20 @@ $ClashConfigCopyWith<ClashConfig> get copyWith => _$ClashConfigCopyWithImpl<Clas
@override
bool operator ==(Object other) {
final _this = this as ClashConfig;
return identical(this, other) || (other.runtimeType == runtimeType&&other is ClashConfig&&const DeepCollectionEquality().equals(other.proxyGroups, _this.proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _this.rules)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.proxyProviders, _this.proxyProviders)&&const DeepCollectionEquality().equals(other.ruleProviders, _this.ruleProviders)&&const DeepCollectionEquality().equals(other.subRules, _this.subRules)&&const DeepCollectionEquality().equals(other.proxyTypeMap, _this.proxyTypeMap));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ClashConfig&&const DeepCollectionEquality().equals(other.proxyGroups, _this.proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _this.rules)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.proxyProviders, _this.proxyProviders)&&const DeepCollectionEquality().equals(other.ruleProviders, _this.ruleProviders)&&const DeepCollectionEquality().equals(other.subRules, _this.subRules));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as ClashConfig;
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.proxyGroups),const DeepCollectionEquality().hash(_this.rules),const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.proxyProviders),const DeepCollectionEquality().hash(_this.ruleProviders),const DeepCollectionEquality().hash(_this.subRules),const DeepCollectionEquality().hash(_this.proxyTypeMap));
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.proxyGroups),const DeepCollectionEquality().hash(_this.rules),const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.proxyProviders),const DeepCollectionEquality().hash(_this.ruleProviders),const DeepCollectionEquality().hash(_this.subRules));
}
@override
String toString() {
final _this = this as ClashConfig;
return 'ClashConfig(proxyGroups: ${_this.proxyGroups}, rules: ${_this.rules}, proxies: ${_this.proxies}, proxyProviders: ${_this.proxyProviders}, ruleProviders: ${_this.ruleProviders}, subRules: ${_this.subRules}, proxyTypeMap: ${_this.proxyTypeMap})';
return 'ClashConfig(proxyGroups: ${_this.proxyGroups}, rules: ${_this.rules}, proxies: ${_this.proxies}, proxyProviders: ${_this.proxyProviders}, ruleProviders: ${_this.ruleProviders}, subRules: ${_this.subRules})';
}
@@ -7517,7 +6974,7 @@ abstract mixin class $ClashConfigCopyWith<$Res> {
factory $ClashConfigCopyWith(ClashConfig value, $Res Function(ClashConfig) _then) = _$ClashConfigCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups,@JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies,@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders,@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders,@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules, Map<String, String> proxyTypeMap
@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups,@JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies,@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders,@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders,@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules
});
@@ -7534,7 +6991,7 @@ class _$ClashConfigCopyWithImpl<$Res>
/// Create a copy of ClashConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? proxyGroups = null,Object? rules = null,Object? proxies = null,Object? proxyProviders = null,Object? ruleProviders = null,Object? subRules = null,Object? proxyTypeMap = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? proxyGroups = null,Object? rules = null,Object? proxies = null,Object? proxyProviders = null,Object? ruleProviders = null,Object? subRules = null,}) {
return _then(ClashConfig(
proxyGroups: null == proxyGroups ? _self.proxyGroups : proxyGroups // ignore: cast_nullable_to_non_nullable
as List<ProxyGroup>,rules: null == rules ? _self.rules : rules // ignore: cast_nullable_to_non_nullable
@@ -7542,8 +6999,7 @@ as List<Rule>,proxies: null == proxies ? _self.proxies : proxies // ignore: cast
as List<Proxy>,proxyProviders: null == proxyProviders ? _self.proxyProviders : proxyProviders // ignore: cast_nullable_to_non_nullable
as List<String>,ruleProviders: null == ruleProviders ? _self.ruleProviders : ruleProviders // ignore: cast_nullable_to_non_nullable
as List<String>,subRules: null == subRules ? _self.subRules : subRules // ignore: cast_nullable_to_non_nullable
as List<String>,proxyTypeMap: null == proxyTypeMap ? _self.proxyTypeMap : proxyTypeMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
as List<String>,
));
}
@@ -7628,10 +7084,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules, Map<String, String> proxyTypeMap)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ClashConfig() when $default != null:
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules,_that.proxyTypeMap);case _:
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules);case _:
return orElse();
}
@@ -7649,10 +7105,10 @@ return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules, Map<String, String> proxyTypeMap) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules) $default,) {final _that = this;
switch (_that) {
case _ClashConfig():
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules,_that.proxyTypeMap);case _:
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules);case _:
throw StateError('Unexpected subclass');
}
@@ -7669,10 +7125,10 @@ return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules, Map<String, String> proxyTypeMap)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups, @JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies, @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders, @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders, @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules)? $default,) {final _that = this;
switch (_that) {
case _ClashConfig() when $default != null:
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules,_that.proxyTypeMap);case _:
return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders,_that.ruleProviders,_that.subRules);case _:
return null;
}
@@ -7684,7 +7140,7 @@ return $default(_that.proxyGroups,_that.rules,_that.proxies,_that.proxyProviders
@JsonSerializable()
class _ClashConfig implements ClashConfig {
const _ClashConfig({@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups = const [], @JsonKey(fromJson: _genRules) List<Rule> rules = const [], List<Proxy> proxies = const [], @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders = const [], @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders = const [], @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules = const [], Map<String, String> proxyTypeMap = const {}}): _proxyGroups = proxyGroups,_rules = rules,_proxies = proxies,_proxyProviders = proxyProviders,_ruleProviders = ruleProviders,_subRules = subRules,_proxyTypeMap = proxyTypeMap;
const _ClashConfig({@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups = const [], @JsonKey(fromJson: _genRules) List<Rule> rules = const [], List<Proxy> proxies = const [], @JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders = const [], @JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders = const [], @JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules = const []}): _proxyGroups = proxyGroups,_rules = rules,_proxies = proxies,_proxyProviders = proxyProviders,_ruleProviders = ruleProviders,_subRules = subRules;
factory _ClashConfig.fromJson(Map<String, dynamic> json) => _$ClashConfigFromJson(json);
final List<ProxyGroup> _proxyGroups;
@@ -7729,13 +7185,6 @@ class _ClashConfig implements ClashConfig {
return EqualUnmodifiableListView(_subRules);
}
final Map<String, String> _proxyTypeMap;
@override@JsonKey() Map<String, String> get proxyTypeMap {
if (_proxyTypeMap is EqualUnmodifiableMapView) return _proxyTypeMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_proxyTypeMap);
}
/// Create a copy of ClashConfig
/// with the given fields replaced by the non-null parameter values.
@@ -7750,18 +7199,18 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ClashConfig&&const DeepCollectionEquality().equals(other.proxyGroups, _proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _rules)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.proxyProviders, _proxyProviders)&&const DeepCollectionEquality().equals(other.ruleProviders, _ruleProviders)&&const DeepCollectionEquality().equals(other.subRules, _subRules)&&const DeepCollectionEquality().equals(other.proxyTypeMap, _proxyTypeMap));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ClashConfig&&const DeepCollectionEquality().equals(other.proxyGroups, _proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _rules)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.proxyProviders, _proxyProviders)&&const DeepCollectionEquality().equals(other.ruleProviders, _ruleProviders)&&const DeepCollectionEquality().equals(other.subRules, _subRules));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_proxyGroups),const DeepCollectionEquality().hash(_rules),const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_proxyProviders),const DeepCollectionEquality().hash(_ruleProviders),const DeepCollectionEquality().hash(_subRules),const DeepCollectionEquality().hash(_proxyTypeMap));
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_proxyGroups),const DeepCollectionEquality().hash(_rules),const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_proxyProviders),const DeepCollectionEquality().hash(_ruleProviders),const DeepCollectionEquality().hash(_subRules));
}
@override
String toString() {
return 'ClashConfig(proxyGroups: $proxyGroups, rules: $rules, proxies: $proxies, proxyProviders: $proxyProviders, ruleProviders: $ruleProviders, subRules: $subRules, proxyTypeMap: $proxyTypeMap)';
return 'ClashConfig(proxyGroups: $proxyGroups, rules: $rules, proxies: $proxies, proxyProviders: $proxyProviders, ruleProviders: $ruleProviders, subRules: $subRules)';
}
@@ -7772,7 +7221,7 @@ abstract mixin class _$ClashConfigCopyWith<$Res> implements $ClashConfigCopyWith
factory _$ClashConfigCopyWith(_ClashConfig value, $Res Function(_ClashConfig) _then) = __$ClashConfigCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups,@JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies,@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders,@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders,@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules, Map<String, String> proxyTypeMap
@JsonKey(name: 'proxy-groups') List<ProxyGroup> proxyGroups,@JsonKey(fromJson: _genRules) List<Rule> rules, List<Proxy> proxies,@JsonKey(name: 'proxy-providers', fromJson: _genList) List<String> proxyProviders,@JsonKey(name: 'rule-providers', fromJson: _genList) List<String> ruleProviders,@JsonKey(name: 'sub-rules', fromJson: _genList) List<String> subRules
});
@@ -7789,7 +7238,7 @@ class __$ClashConfigCopyWithImpl<$Res>
/// Create a copy of ClashConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? proxyGroups = null,Object? rules = null,Object? proxies = null,Object? proxyProviders = null,Object? ruleProviders = null,Object? subRules = null,Object? proxyTypeMap = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? proxyGroups = null,Object? rules = null,Object? proxies = null,Object? proxyProviders = null,Object? ruleProviders = null,Object? subRules = null,}) {
return _then(_ClashConfig(
proxyGroups: null == proxyGroups ? _self._proxyGroups : proxyGroups // ignore: cast_nullable_to_non_nullable
as List<ProxyGroup>,rules: null == rules ? _self._rules : rules // ignore: cast_nullable_to_non_nullable
@@ -7797,8 +7246,7 @@ as List<Rule>,proxies: null == proxies ? _self._proxies : proxies // ignore: cas
as List<Proxy>,proxyProviders: null == proxyProviders ? _self._proxyProviders : proxyProviders // ignore: cast_nullable_to_non_nullable
as List<String>,ruleProviders: null == ruleProviders ? _self._ruleProviders : ruleProviders // ignore: cast_nullable_to_non_nullable
as List<String>,subRules: null == subRules ? _self._subRules : subRules // ignore: cast_nullable_to_non_nullable
as List<String>,proxyTypeMap: null == proxyTypeMap ? _self._proxyTypeMap : proxyTypeMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
as List<String>,
));
}
+2 -22
View File
@@ -33,9 +33,8 @@ _ProxyGroup _$ProxyGroupFromJson(Map<String, dynamic> json) => _ProxyGroup(
hashKey: json['hash-key'] as String?,
defaultSelected: json['default-selected'] as String?,
emptyFallback: json['empty-fallback'] as String?,
includeAll: json['include-all'] as bool?,
includeAllProxies: json['include-all-proxies'] as bool?,
includeAllProviders: json['include-all-providers'] as bool?,
includeAllProxies: _readIncludeAll(json, 'include-all-proxies') as bool?,
includeAllProviders: _readIncludeAll(json, 'include-all-providers') as bool?,
hidden: json['hidden'] as bool?,
icon: json['icon'] as String?,
order: json['order'] as String?,
@@ -68,7 +67,6 @@ Map<String, dynamic> _$ProxyGroupToJson(_ProxyGroup instance) =>
'hash-key': instance.hashKey,
'default-selected': instance.defaultSelected,
'empty-fallback': instance.emptyFallback,
'include-all': instance.includeAll,
'include-all-proxies': instance.includeAllProxies,
'include-all-providers': instance.includeAllProviders,
'hidden': instance.hidden,
@@ -186,18 +184,6 @@ const _$ProxyFieldEnumMap = {
ProxyField.ipVersion: 'ipVersion',
};
_RuleProvider _$RuleProviderFromJson(Map<String, dynamic> json) =>
_RuleProvider(name: json['name'] as String);
Map<String, dynamic> _$RuleProviderToJson(_RuleProvider instance) =>
<String, dynamic>{'name': instance.name};
_ProxyProvider _$ProxyProviderFromJson(Map<String, dynamic> json) =>
_ProxyProvider(name: json['name'] as String);
Map<String, dynamic> _$ProxyProviderToJson(_ProxyProvider instance) =>
<String, dynamic>{'name': instance.name};
_Sniffer _$SnifferFromJson(Map<String, dynamic> json) => _Sniffer(
enable: json['enable'] as bool? ?? false,
overrideDest: json['override-destination'] as bool? ?? true,
@@ -771,11 +757,6 @@ _ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => _ClashConfig(
subRules: json['sub-rules'] == null
? const []
: _genList(json['sub-rules'] as Map<String, dynamic>),
proxyTypeMap:
(json['proxyTypeMap'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const {},
);
Map<String, dynamic> _$ClashConfigToJson(_ClashConfig instance) =>
@@ -786,7 +767,6 @@ Map<String, dynamic> _$ClashConfigToJson(_ClashConfig instance) =>
'proxy-providers': instance.proxyProviders,
'rule-providers': instance.ruleProviders,
'sub-rules': instance.subRules,
'proxyTypeMap': instance.proxyTypeMap,
};
_PatchClashConfig _$PatchClashConfigFromJson(Map<String, dynamic> json) =>
+9
View File
@@ -144,6 +144,15 @@ extension ProfileExtension on Profile {
String get fileName => '$id.yaml';
String get exportFileName {
final label = realLabel;
final lower = label.toLowerCase();
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
return label;
}
return '$label.yaml';
}
String get updatingKey => 'profile_$id';
Future<Profile?> checkAndUpdateAndCopy({
+22 -16
View File
@@ -40,8 +40,11 @@ class ProfilesAction extends _$ProfilesAction {
}
Future<void> autoUpdateProfiles() async {
for (final profile in ref.read(profilesProvider)) {
if (!profile.autoUpdate) continue;
for (final id in [
for (final profile in ref.read(profilesProvider)) profile.id,
]) {
final profile = ref.read(profilesProvider).getProfile(id);
if (profile == null || !profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(profile.autoUpdateDuration)
.isBeforeNow;
@@ -123,8 +126,11 @@ class ProfilesAction extends _$ProfilesAction {
}
Future<void> updateProfiles() async {
for (final profile in ref.read(profilesProvider)) {
if (profile.type != ProfileType.url) continue;
for (final id in [
for (final profile in ref.read(profilesProvider)) profile.id,
]) {
final profile = ref.read(profilesProvider).getProfile(id);
if (profile == null || profile.type != ProfileType.url) continue;
await updateProfile(profile, showLoading: true);
}
}
@@ -138,9 +144,20 @@ class ProfilesAction extends _$ProfilesAction {
: null;
try {
ref.read(profilesProvider.notifier).put(profile);
final newProfile = await profile.update(
final updated = await profile.update(
validate: (path, key) => _core.validateProfile(path, key),
);
// The download can take minutes; edits and deletes made meanwhile win.
final latest = ref.read(profilesProvider).getProfile(profile.id);
if (latest == null) {
await clearEffect(profile.id);
return;
}
final newProfile = latest.copyWith(
label: latest.label.takeFirstValid([updated.label]),
subscriptionInfo: updated.subscriptionInfo,
lastUpdateDate: updated.lastUpdateDate,
);
ref.read(profilesProvider.notifier).put(newProfile);
if (profile.id == ref.read(currentProfileIdProvider)) {
ref
@@ -435,23 +452,12 @@ class ProfilesAction extends _$ProfilesAction {
);
}
void setProfileAndAutoApply(Profile profile) {
ref.read(profilesProvider.notifier).put(profile);
if (profile.id == ref.read(currentProfileIdProvider)) {
ref.read(setupActionProvider.notifier).applyProfileDebounce();
}
}
Future<void> addProfileFormQrCode() async {
final url = await globalState.safeRun(picker.pickerConfigQRCode);
if (url == null) return;
unawaited(addProfileFromLink(url));
}
void reorder(List<Profile> profiles) {
ref.read(profilesProvider.notifier).reorder(profiles);
}
Future<void> clearEffect(int profileId) async {
final profilePath = await appPath.getProfilePath(profileId.toString());
final profileFile = File(profilePath);
+5 -22
View File
@@ -5,7 +5,6 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/database/database.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'generated/database.g.dart';
@@ -132,21 +131,11 @@ Stream<List<Profile>> profilesStream(Ref ref) {
return database.profilesDao.query().watch();
}
@riverpod
Stream<List<Rule>> addedRulesStream(Ref ref, int profileId) {
return database.rulesDao.queryAddedRules(profileId).watch();
}
@riverpod
Stream<Set<String>> customGroupNames(Ref ref) {
return database.proxyGroupsDao.names().watch().map(Set.of);
}
@riverpod
Stream<int> proxyGroupsCount(Ref ref, int profileId) {
return database.proxyGroupsDao.count(profileId).watchSingle();
}
@Riverpod(keepAlive: true)
class Profiles extends _$Profiles {
late final _writes = _Writes(_reread);
@@ -517,17 +506,6 @@ class IconSets extends _$IconSets with AsyncNotifierMixin, OptimisticMixin {
}
}
@riverpod
Future<Script?> script(Ref ref, int? scriptId) async {
final script = ref.watch(
scriptsProvider.future.select((state) async {
final scripts = await state;
return scripts.get(scriptId);
}),
);
return script;
}
mixin RuleListMixin on OptimisticMixin<List<Rule>> {
Future<void> persistRule(Rule rule);
@@ -639,6 +617,11 @@ class GlobalRules extends _$GlobalRules
Future<void> persistRule(Rule rule) => database.rulesDao.putRule(rule);
}
@riverpod
Stream<int> profileGroupsCount(Ref ref, int profileId) {
return database.proxyGroupsDao.profileGroupsCount(profileId).watchSingle();
}
/// What the standard extension adds to a profile, or a custom profile's rules.
@riverpod
class ProfileRules extends _$ProfileRules
+70 -212
View File
@@ -48,80 +48,6 @@ final class ProfilesStreamProvider
String _$profilesStreamHash() => r'ea944e081294567f0f63286e95e4e66cdc650383';
@ProviderFor(addedRulesStream)
final addedRulesStreamProvider = AddedRulesStreamFamily._();
final class AddedRulesStreamProvider
extends
$FunctionalProvider<
AsyncValue<List<Rule>>,
List<Rule>,
Stream<List<Rule>>
>
with $FutureModifier<List<Rule>>, $StreamProvider<List<Rule>> {
AddedRulesStreamProvider._({
required AddedRulesStreamFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'addedRulesStreamProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addedRulesStreamHash();
@override
String toString() {
return r'addedRulesStreamProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<List<Rule>> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<List<Rule>> create(Ref ref) {
final argument = this.argument as int;
return addedRulesStream(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AddedRulesStreamProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$addedRulesStreamHash() => r'5d37e4f080094a44c2f6f84dda60d6796f4b3c99';
final class AddedRulesStreamFamily extends $Family
with $FunctionalFamilyOverride<Stream<List<Rule>>, int> {
AddedRulesStreamFamily._()
: super(
retry: null,
name: r'addedRulesStreamProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AddedRulesStreamProvider call(int profileId) =>
AddedRulesStreamProvider._(argument: profileId, from: this);
@override
String toString() => r'addedRulesStreamProvider';
}
@ProviderFor(customGroupNames)
final customGroupNamesProvider = CustomGroupNamesProvider._();
@@ -161,75 +87,6 @@ final class CustomGroupNamesProvider
String _$customGroupNamesHash() => r'9d3d1a6b57aeee7b4cf34afd20e28336484a5de7';
@ProviderFor(proxyGroupsCount)
final proxyGroupsCountProvider = ProxyGroupsCountFamily._();
final class ProxyGroupsCountProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
ProxyGroupsCountProvider._({
required ProxyGroupsCountFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'proxyGroupsCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$proxyGroupsCountHash();
@override
String toString() {
return r'proxyGroupsCountProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as int;
return proxyGroupsCount(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ProxyGroupsCountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$proxyGroupsCountHash() => r'9bf90fc25a9ae3b9ab7aa0784d4e47786f4c4d52';
final class ProxyGroupsCountFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, int> {
ProxyGroupsCountFamily._()
: super(
retry: null,
name: r'proxyGroupsCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ProxyGroupsCountProvider call(int profileId) =>
ProxyGroupsCountProvider._(argument: profileId, from: this);
@override
String toString() => r'proxyGroupsCountProvider';
}
@ProviderFor(Profiles)
final profilesProvider = ProfilesProvider._();
@@ -415,75 +272,6 @@ abstract class _$IconSets extends $StreamNotifier<List<IconSet>> {
}
}
@ProviderFor(script)
final scriptProvider = ScriptFamily._();
final class ScriptProvider
extends $FunctionalProvider<AsyncValue<Script?>, Script?, FutureOr<Script?>>
with $FutureModifier<Script?>, $FutureProvider<Script?> {
ScriptProvider._({
required ScriptFamily super.from,
required int? super.argument,
}) : super(
retry: null,
name: r'scriptProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$scriptHash();
@override
String toString() {
return r'scriptProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Script?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Script?> create(Ref ref) {
final argument = this.argument as int?;
return script(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ScriptProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$scriptHash() => r'c97b48d58cef1bc928cdcfc1b292fd84ef515593';
final class ScriptFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Script?>, int?> {
ScriptFamily._()
: super(
retry: null,
name: r'scriptProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ScriptProvider call(int? scriptId) =>
ScriptProvider._(argument: scriptId, from: this);
@override
String toString() => r'scriptProvider';
}
@ProviderFor(GlobalRules)
final globalRulesProvider = GlobalRulesProvider._();
@@ -528,6 +316,76 @@ abstract class _$GlobalRules extends $StreamNotifier<List<Rule>> {
}
}
@ProviderFor(profileGroupsCount)
final profileGroupsCountProvider = ProfileGroupsCountFamily._();
final class ProfileGroupsCountProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
ProfileGroupsCountProvider._({
required ProfileGroupsCountFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'profileGroupsCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$profileGroupsCountHash();
@override
String toString() {
return r'profileGroupsCountProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as int;
return profileGroupsCount(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ProfileGroupsCountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$profileGroupsCountHash() =>
r'c82d409c32b5467619c51bba28b2066b6ea09c59';
final class ProfileGroupsCountFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, int> {
ProfileGroupsCountFamily._()
: super(
retry: null,
name: r'profileGroupsCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ProfileGroupsCountProvider call(int profileId) =>
ProfileGroupsCountProvider._(argument: profileId, from: this);
@override
String toString() => r'profileGroupsCountProvider';
}
/// What the standard extension adds to a profile, or a custom profile's rules.
@ProviderFor(ProfileRules)
+12 -7
View File
@@ -508,7 +508,7 @@ Set<String> _namesOnLoops(Map<String, List<String>> members) {
}
/// [target] and what under it leads back to [proxy], through a group's own
/// list, include-all where no filter narrows it, or an app proxy's entry in
/// list or, where an app proxy dials through another, that proxy's entry in
/// [dialers]; empty when the dial does not loop. What filters and providers
/// pick is left to the core, which fails a dial that comes back to its dialer.
Set<String> dialerLoop(
@@ -519,7 +519,7 @@ Set<String> dialerLoop(
}) => dialerLoopsOf(groups, proxy: proxy, dialers: dialers)(target);
bool _takesInEveryProxy(ProxyGroup group) =>
(group.includeAll == true || group.includeAllProxies == true) &&
group.includeAllProxies == true &&
(group.filter ?? '').isEmpty &&
(group.excludeFilter ?? '').isEmpty &&
(group.excludeType ?? '').isEmpty;
@@ -566,8 +566,7 @@ class _DialerGraph {
}
}
/// The proxy's own dialer is the dial being checked, never a way back; a
/// proxy without one is still taken in by every include-all group.
/// The proxy's own dialer is the dial being checked, never a way back.
Set<String> Function(String target) loopsOf(String proxy) {
if (!declared.contains(proxy)) {
return (_) => const {};
@@ -792,9 +791,13 @@ List<CustomIssue> proxyGroupIssues(ProxyGroup group, CustomProfileData data) {
final hasSource =
proxies.isNotEmpty ||
use.isNotEmpty ||
group.includeAll == true ||
group.includeAllProxies == true ||
group.includeAllProviders == true;
group.includeAllProviders == true &&
data.proxyGroups.any(
(item) =>
item.id != group.id &&
(item.use?.any(data.proxyProviders.contains) ?? false),
);
final missingProxies = [
for (final item in proxies)
if (!data.ruleTargets.contains(item) && !data.proxies.contains(item))
@@ -804,7 +807,9 @@ List<CustomIssue> proxyGroupIssues(ProxyGroup group, CustomProfileData data) {
// The core takes only a proxy here, never a group, and refuses the whole
// config otherwise.
final invalidEmptyFallback =
emptyFallback.isNotEmpty && !reservedProxyNames.contains(emptyFallback);
emptyFallback.isNotEmpty &&
!reservedProxyNames.contains(emptyFallback) &&
!data.namedProxies.contains(emptyFallback);
final missingProviders = use
.where((item) => !data.proxyProviders.contains(item))
.toList();
+14 -9
View File
@@ -50,6 +50,8 @@ class CustomProfileImport {
final List<Rule> rules;
final ProfileOverrides overrides;
final int droppedSubRules;
const CustomProfileImport({
required this.proxyProviders,
required this.ruleProviders,
@@ -58,6 +60,7 @@ class CustomProfileImport {
required this.proxyGroups,
required this.rules,
required this.overrides,
this.droppedSubRules = 0,
});
}
@@ -114,6 +117,14 @@ CustomProfileImport readCustomProfileImport(
groupNames: app.groupNames,
);
final ruleSets = ruleProviders.names;
final rules = [
for (final item in _listOf(document['rules']))
_importRule(item.toString(), names: proxies.names, ruleSets: ruleSets),
];
final keptRules = [
for (final rule in rules)
if (rule.ruleAction != RuleAction.SUB_RULE) rule,
];
return CustomProfileImport(
proxyProviders: proxyProviders.added,
ruleProviders: ruleProviders.added,
@@ -128,10 +139,8 @@ CustomProfileImport readCustomProfileImport(
use: proxyProviders.names,
),
],
rules: [
for (final item in _listOf(document['rules']))
_importRule(item.toString(), names: proxies.names, ruleSets: ruleSets),
],
rules: keptRules,
droppedSubRules: rules.length - keptRules.length,
overrides: _importOverrides(
document,
names: proxies.names,
@@ -412,11 +421,7 @@ ProxyGroup _importGroup(
required Map<String, String> use,
}) {
final json = _stringified({
// A custom profile has no include-all-providers, so include-all keeps
// only its proxies.
for (final MapEntry(:key, :value) in group.entries)
if (key != 'include-all' && key != 'include-all-providers') key: value,
if (group['include-all'] == true) 'include-all-proxies': true,
...group,
'id': 0,
'name': name,
if (group.containsKey('proxies'))
+4 -2
View File
@@ -1,11 +1,12 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/pages/scan.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/views/profiles/custom/custom.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -56,7 +57,8 @@ class AddProfileView extends ConsumerWidget {
Future<void> _toAdd(WidgetRef ref) async {
final profilesAction = ref.read(profilesActionProvider.notifier);
final appLocalizations = context.appLocalizations;
final res = await dialogs.showNamedUrlsInput(
final res = await showNamedUrlsForm(
context,
title: appLocalizations.importFromLink,
urlLabel: appLocalizations.link,
batchTip: appLocalizations.batchLinkInputTip,
+95 -174
View File
@@ -3,18 +3,18 @@ import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/views/config/dns.dart';
import 'package:fl_clash/views/config/ntp.dart';
import 'package:fl_clash/views/config/sniffer.dart';
import 'package:fl_clash/views/config/tun.dart';
import 'package:fl_clash/views/profiles/edit.dart';
import 'package:fl_clash/views/profiles/preview.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -42,6 +42,22 @@ void _push(BuildContext context, Widget view) {
BaseNavigator.push(context, view);
}
Widget _sectionRow(
BuildContext context,
Glyph glyph,
String title,
Widget state,
Widget view,
) {
return ListItem(
leading: GlyphIcon(glyph),
title: Text(title),
subtitle: state,
trailing: const GlyphIcon(AppGlyphs.chevronForward),
onTap: () => _push(context, view),
);
}
class CustomProfileView extends ConsumerStatefulWidget {
final int profileId;
@@ -58,8 +74,7 @@ class CustomProfileView extends ConsumerStatefulWidget {
ConsumerState<CustomProfileView> createState() => _CustomProfileViewState();
}
class _CustomProfileViewState extends ConsumerState<CustomProfileView>
with RouteSettledMixin<CustomProfileView> {
class _CustomProfileViewState extends ConsumerState<CustomProfileView> {
late SetupAction _setupAction;
late ProfilesAction _profilesAction;
late final Profile? _initialProfile;
@@ -81,7 +96,7 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
}
bool get _isUntouched {
if (!routeSettled) {
if (!_loaded) {
return true;
}
final profileId = widget.profileId;
@@ -102,9 +117,6 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
];
}
@override
void didSettleRoute() => setState(() {});
bool get _isEmpty {
final profileId = widget.profileId;
return ref.read(profileProvider(profileId))?.overrides ==
@@ -155,12 +167,30 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
if (!_isEmpty) {
final res = await dialogs.showMessage(
message: TextSpan(text: appLocalizations.importConfigReplaceTip),
destructive: true,
confirmText: appLocalizations.replace,
);
if (res != true || !mounted) {
return;
}
}
await _profilesAction.importCustomProfile(widget.profileId, config);
final dropped = [
if (config.droppedSubRules > 0)
appLocalizations.customImportDroppedSubRules(
config.droppedSubRules,
config.droppedSubRules.compact,
),
];
if (dropped.isNotEmpty && mounted) {
unawaited(
dialogs.showMessage(
title: appLocalizations.import,
message: TextSpan(text: dropped.join('\n\n')),
cancelable: false,
),
);
}
} finally {
if (mounted) {
setState(() {
@@ -190,6 +220,20 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
return true;
}
void _handleRename() {
final profile = ref.read(profileProvider(widget.profileId));
if (profile != null) {
unawaited(renameProfile(context, ref, profile));
}
}
void _handlePreview() {
final profile = ref.read(profileProvider(widget.profileId));
if (profile != null) {
_push(context, PreviewProfileView(profile: profile));
}
}
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
@@ -199,12 +243,9 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
profileId,
).select((state) => state?.realLabel ?? _initialProfile?.realLabel ?? ''),
);
// Reads nothing until the route settles, since loading mid-transition
// drops frames.
_loaded =
_loaded ||
(routeSettled &&
ref.watch(
(ref.watch(
customProfileDataProvider(
profileId,
).select((state) => state != null),
@@ -224,14 +265,23 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
profileId: profileId,
child: CommonScaffold(
title: label,
titleMenuItems: [
CommonPopupMenuItem(
glyph: AppGlyphs.textShort,
label: appLocalizations.rename,
onPressed: _handleRename,
),
CommonPopupMenuItem(
glyph: AppGlyphs.eye,
label: appLocalizations.runtimeConfig,
onPressed: _handlePreview,
),
],
isLoading: _importing,
actions: [
CommonPopupBox(
targetBuilder: (open) => FilledButton(
onPressed: _loaded && !_importing
? () =>
open(offset: Offset(0, context.isMobileView ? 0 : 20))
: null,
onPressed: _loaded && !_importing ? open : null,
child: Text(appLocalizations.import),
),
popupBuilder: (_) => CommonPopupMenu(
@@ -251,8 +301,8 @@ class _CustomProfileViewState extends ConsumerState<CustomProfileView>
),
],
body: NullStatusSwitcher(
isEmpty: !_loaded,
nullStatus: const NullStatus(
isLoading: !_loaded,
loadingStatus: const NullStatus(
illustration: NullStatusIllustration.profile,
),
child: ScrollConfiguration(
@@ -331,15 +381,9 @@ class _Overview extends ConsumerWidget {
_MoreSectionsView(profileId, showsTun: showsTun),
),
];
Widget tile(int index, Widget value, {bool invalid = false}) {
final (glyph, label, view) = sections[index];
return _OverviewTile(
glyph: glyph,
label: label,
invalid: invalid,
onPressed: () => _push(context, view),
value: value,
);
Widget row(int index, Widget state) {
final (glyph, title, view) = sections[index];
return _sectionRow(context, glyph, title, state, view);
}
ref.listen(customProfileDataProvider(profileId), (_, _) {});
@@ -392,38 +436,33 @@ class _Overview extends ConsumerWidget {
? appLocalizations.customIssuesSummary(issueCount)
: null,
),
_TileGrid(
children: [
tile(
0,
_CountText(count: ruleNum, issueCount: issueCounts.rules),
invalid: issueCounts.rules > 0,
),
tile(
generateSection(
items: [
row(0, _CountText(count: ruleNum, issueCount: issueCounts.rules)),
row(
1,
_CountText(
count: providers.proxy.length + providers.rule.length,
issueCount: missingProviders,
),
invalid: missingProviders > 0,
),
tile(
row(
2,
_StatusText(
_enabledText(appLocalizations, status.dns),
active: status.dns,
invalid: issueCounts.dns > 0,
),
invalid: issueCounts.dns > 0,
),
tile(
row(
3,
_StatusText(
moreInEffect > 0
? appLocalizations.sectionsInEffect(moreInEffect)
: appLocalizations.defaultText,
active: moreInEffect > 0,
invalid: issueCounts.dialers + issueCounts.ntp > 0,
),
invalid: issueCounts.dialers + issueCounts.ntp > 0,
),
],
),
@@ -474,15 +513,6 @@ class _MoreSectionsView extends ConsumerWidget {
),
),
);
Widget section(Glyph glyph, String title, Widget state, Widget view) {
return ListItem(
leading: GlyphIcon(glyph),
title: Text(title),
subtitle: state,
onTap: () => _push(context, view),
);
}
return CommonScaffold(
title: appLocalizations.more,
body: ListView(
@@ -490,9 +520,10 @@ class _MoreSectionsView extends ConsumerWidget {
horizontal: 16,
).copyWith(top: context.contentTopPadding, bottom: 16),
children: [
generateSectionV3(
generateSection(
items: [
section(
_sectionRow(
context,
AppGlyphs.link,
appLocalizations.dialerProxy,
Text(
@@ -508,7 +539,8 @@ class _MoreSectionsView extends ConsumerWidget {
),
CustomProxyDialersView(profileId),
),
section(
_sectionRow(
context,
AppGlyphs.clock,
'NTP',
Text(
@@ -519,14 +551,16 @@ class _MoreSectionsView extends ConsumerWidget {
),
NtpView(profileId),
),
section(
_sectionRow(
context,
AppGlyphs.eye,
appLocalizations.sniffer,
Text(_enabledText(appLocalizations, status.sniffer)),
SnifferView(profileId),
),
if (showsTun)
section(
_sectionRow(
context,
AppGlyphs.vpn,
appLocalizations.tun,
Text(
@@ -544,124 +578,6 @@ class _MoreSectionsView extends ConsumerWidget {
}
}
/// Balances its rows, so four tiles that fit three abreast make two rows of
/// two rather than three and one.
class _TileGrid extends StatelessWidget {
const _TileGrid({required this.children});
final List<Widget> children;
static const _spacing = 8.0;
static const _minTileWidth = 150.0;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final fit =
((constraints.maxWidth + _spacing) ~/ (_minTileWidth + _spacing))
.clamp(1, children.length);
final rows = (children.length / fit).ceil();
final columns = (children.length / rows).ceil();
return Column(
spacing: _spacing,
children: [
for (var start = 0; start < children.length; start += columns)
Row(
spacing: _spacing,
children: [
for (var index = start; index < start + columns; index++)
Expanded(
child: index < children.length
? children[index]
: const SizedBox.shrink(),
),
],
),
],
);
},
);
}
}
class _OverviewTile extends StatelessWidget {
const _OverviewTile({
required this.glyph,
required this.label,
required this.onPressed,
required this.value,
this.invalid = false,
});
final Glyph glyph;
final String label;
final VoidCallback onPressed;
final Widget value;
final bool invalid;
@override
Widget build(BuildContext context) {
final colorScheme = context.colorScheme;
final textTheme = context.textTheme;
return CommonCard(
type: context.isInBottomSheet
? CommonCardType.filled
: CommonCardType.plain,
radius: AppCorner.lg,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
spacing: 12,
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: ShapeDecoration(
color: invalid
? colorScheme.errorContainer
: colorScheme.secondaryContainer,
shape: AppShape.circle,
),
child: GlyphIcon(
glyph,
size: 20,
color: invalid
? colorScheme.onErrorContainer
: colorScheme.onSecondaryContainer,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
DefaultTextStyle.merge(
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: value,
),
],
),
),
],
),
),
);
}
}
class _CountText extends StatelessWidget {
const _CountText({required this.count, this.issueCount = 0});
@@ -682,10 +598,11 @@ class _CountText extends StatelessWidget {
}
class _StatusText extends StatelessWidget {
const _StatusText(this.text, {required this.active});
const _StatusText(this.text, {required this.active, this.invalid = false});
final String text;
final bool active;
final bool invalid;
@override
Widget build(BuildContext context) {
@@ -693,7 +610,11 @@ class _StatusText extends StatelessWidget {
return Text(
text,
style: TextStyle(
color: active ? colorScheme.tertiary : colorScheme.onSurfaceVariant,
color: invalid
? colorScheme.error
: active
? colorScheme.tertiary
: colorScheme.onSurfaceVariant,
),
);
}
+30 -29
View File
@@ -4,11 +4,11 @@ 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/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart' hide FileInfo;
import 'package:fl_clash/pages/editor.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -225,7 +225,11 @@ class _CustomProxiesViewState extends ConsumerState<CustomProxiesView> {
if (!mounted) {
return;
}
final res = await dialogs.showMessage(message: TextSpan(text: message));
final res = await dialogs.showMessage(
message: TextSpan(text: message),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res != true || !mounted) {
return;
}
@@ -318,6 +322,7 @@ class _CustomProxiesViewState extends ConsumerState<CustomProxiesView> {
added.join(', '),
),
),
destructive: true,
);
return res == true;
}
@@ -428,12 +433,11 @@ class _CustomProxiesViewState extends ConsumerState<CustomProxiesView> {
nullStatus: NullStatus(
label: appLocalizations.nullTip(appLocalizations.proxies),
illustration: NullStatusIllustration.proxies,
action: ElasticButton(
child: FilledButton.tonalIcon(
onPressed: _handleAddOrUpdate,
icon: const GlyphIcon(AppGlyphs.addCircle, fill: 1),
label: Text(appLocalizations.add),
),
action: TonalButton(
size: TonalButtonSize.bar,
glyph: AppGlyphs.addCircle,
label: appLocalizations.add,
onPressed: _handleAddOrUpdate,
),
),
child: Builder(
@@ -646,6 +650,7 @@ class _ShareLinkDialogState extends State<_ShareLinkDialog> {
final appLocalizations = context.appLocalizations;
return CommonDialog(
title: appLocalizations.quickEdit,
hasFields: true,
actions: [
TextButton(
onPressed: _reading ? null : _submit,
@@ -656,11 +661,9 @@ class _ShareLinkDialogState extends State<_ShareLinkDialog> {
controller: _controller,
autofocus: true,
minLines: 3,
maxLines: 6,
maxLines: null,
keyboardType: TextInputType.url,
decoration: InputDecoration(
labelText: 'URI',
alignLabelWithHint: true,
hintText: 'vless://…',
errorText: _error,
errorMaxLines: 3,
@@ -971,10 +974,9 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
ValueChanged<Object?>? onChanged,
}) {
final update = onChanged ?? (Object? value) => _update(key, value);
final field = TextFormField(
final field = FormRowTextField(
key: ValueKey('$key-$_revision'),
initialValue: value,
textAlign: TextAlign.end,
obscureText: obscureText,
keyboardType: digitsOnly ? TextInputType.number : TextInputType.text,
inputFormatters: digitsOnly
@@ -985,10 +987,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
onChanged: (value) {
update(digitsOnly ? int.tryParse(value) : value);
},
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: hintText ?? context.appLocalizations.optional,
),
hintText: hintText ?? context.appLocalizations.optional,
);
return FormRow(
title: title,
@@ -1163,7 +1162,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
Flexible(
child: Text(
value,
style: context.listTitleStyle?.copyWith(
style: context.listTitleStyle.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
@@ -1300,7 +1299,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
Widget _buildEasyTierSection(CustomProxy proxy) {
final appLocalizations = context.appLocalizations;
final definition = proxy.definition;
return generateSectionV3(
return generateSection(
title: appLocalizations.network,
items: [
_buildTextItem(
@@ -1353,7 +1352,11 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
if (!mounted) {
return;
}
final res = await dialogs.showMessage(message: TextSpan(text: message));
final res = await dialogs.showMessage(
message: TextSpan(text: message),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res == true && mounted) {
ref.read(customProxiesProvider.notifier).delAll([id]);
context.safeNestedPop();
@@ -1409,11 +1412,10 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
final easyTier = proxy.type == 'easytier';
final fields = proxy.fields;
return CommonScaffold(
iconActions: customFormActions(
context,
onQuickEdit: _handleQuickEdit,
onSave: _handleSave,
),
iconActions: [
customQuickEditAction(context, onPressed: _handleQuickEdit),
],
confirmationAction: customSaveAction(context, onPressed: _handleSave),
body: CustomScrollView(
slivers: [
SliverCustomIssuesBanner(issues: issues),
@@ -1423,7 +1425,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
).copyWith(bottom: 20),
sliver: SliverList.list(
children: [
generateSectionV3(
generateSection(
title: appLocalizations.basicInfo,
items: [
_buildTextItem(
@@ -1463,7 +1465,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
],
),
if (easyTier) _buildEasyTierSection(proxy),
generateSectionV3(
generateSection(
title: appLocalizations.authentication,
items: [
for (final field in fields)
@@ -1471,7 +1473,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
],
),
_buildOptions(proxy),
generateSectionV3(
generateSection(
title: appLocalizations.action,
items: [
if (!_isNew)
@@ -1546,7 +1548,6 @@ class _ProxyValuesView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ListEditorPage<String, String>(
title: title,
selectionEnabled: true,
idOf: (value) => value,
itemsOf: (ref) => ref.watch(
customProxyProvider.select((state) => state.stringsOf(field)),
-1
View File
@@ -1,5 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
+69 -79
View File
@@ -5,10 +5,10 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/models/models.dart' hide FileInfo;
import 'package:fl_clash/pages/editor.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -210,6 +210,8 @@ class _CustomProxyGroupsSectionState
? message
: appLocalizations.proxyGroupInUse(inUse.join(', ')),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res != true || !mounted) {
return;
@@ -257,55 +259,34 @@ class _CustomProxyGroupsSectionState
];
}
List<Widget> _buildActions(Set<int> ids, Set<int> selected) {
List<IconButtonData> _buildActions(Set<int> ids, Set<int> selected) {
final appLocalizations = context.appLocalizations;
if (selected.isNotEmpty) {
return [
TonalButtonGroup(
size: TonalButtonSize.section,
children: [
IconButton(
tooltip: appLocalizations.delete,
onPressed: () => _handleDelete(
selected,
message: appLocalizations.deleteMultipTip(
appLocalizations.proxyGroup,
),
),
icon: const GlyphIcon(AppGlyphs.delete),
IconButtonData(
glyph: AppGlyphs.delete,
tooltip: appLocalizations.delete,
onPressed: () => _handleDelete(
selected,
message: appLocalizations.deleteMultipTip(
appLocalizations.proxyGroup,
),
IconButton(
tooltip: appLocalizations.selectAll,
onPressed: () => _handleSelectAll(ids),
icon: const GlyphIcon(AppGlyphs.selectAll),
),
],
),
),
IconButtonData(
glyph: AppGlyphs.selectAll,
tooltip: appLocalizations.selectAll,
onPressed: () => _handleSelectAll(ids),
),
];
}
return [
if (_sorting || ids.length > 1)
TonalButtonTheme(
size: TonalButtonSize.section,
child: ElasticPress(
child: AppBarActionButton(
data: sortModeAction(
context,
sorting: _sorting,
onPressed: _toggleSorting,
),
),
),
),
TonalButtonTheme(
size: TonalButtonSize.section,
child: ElasticPress(
child: IconButton(
tooltip: appLocalizations.add,
onPressed: _handleAddOrUpdate,
icon: const GlyphIcon(AppGlyphs.addCircle),
),
),
sortModeAction(context, sorting: _sorting, onPressed: _toggleSorting),
IconButtonData(
glyph: AppGlyphs.addCircle,
tooltip: appLocalizations.add,
onPressed: _handleAddOrUpdate,
),
];
}
@@ -353,9 +334,11 @@ class _CustomProxyGroupsSectionState
child: SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: ListHeader(
title: appLocalizations.proxyGroup,
actions: _buildActions(ids, selected),
child: TonalButtonTheme(
child: ListHeader(
title: appLocalizations.proxyGroup,
actions: _buildActions(ids, selected),
),
),
),
if (proxyGroups != null && proxyGroups.isEmpty)
@@ -636,10 +619,16 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
}
Future<void> _showEmptyFallbackOptions() async {
final profileId = ProfileIdProvider.of(context)!.profileId;
final res = await Navigator.of(context).push(
PagedSheetRoute(
builder: (context) => Consumer(
builder: (_, ref, _) {
final proxies = ref.watch(
customProfileDataProvider(
profileId,
).select((data) => data?.namedProxies ?? const <String>{}),
);
return SelectionSheet<String>(
title: context.appLocalizations.emptyFallback,
sections: [
@@ -647,6 +636,10 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
label: context.appLocalizations.basicStrategy,
items: reservedProxyNames.toList(),
),
SelectionSection(
label: context.appLocalizations.proxies,
items: proxies.toList(),
),
],
labelBuilder: (item) => item,
selectedOf: (ref) =>
@@ -745,20 +738,16 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
String? suffix,
}) {
final appLocalizations = context.appLocalizations;
final field = TextFormField(
final field = FormRowTextField(
keyboardType: TextInputType.number,
inputFormatters: TextInputLimits.digitsOnly(TextInputLimits.number),
textAlign: TextAlign.end,
initialValue: value?.toString(),
onChanged: (value) {
ref
.read(proxyGroupProvider.notifier)
.update((state) => apply(state, int.tryParse(value)));
},
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: appLocalizations.optional,
),
hintText: appLocalizations.optional,
);
return _buildItem(
title: title,
@@ -770,7 +759,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
spacing: 4,
children: [
Flexible(child: field),
Text(suffix, style: context.textTheme.bodyMedium),
Text(suffix, style: AppTextStyle.subheadline.of(context)),
],
),
);
@@ -781,10 +770,9 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
return _buildItem(
title: appLocalizations.testUrl,
leading: leading,
trailing: TextFormField(
trailing: FormRowTextField(
keyboardType: TextInputType.url,
inputFormatters: TextInputLimits.limit(TextInputLimits.url),
textAlign: TextAlign.end,
initialValue: url,
onChanged: (value) {
ref
@@ -793,10 +781,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
(state) => state.copyWith(url: value.isEmpty ? null : value),
);
},
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: appLocalizations.optional,
),
hintText: appLocalizations.optional,
),
);
}
@@ -865,8 +850,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
return _buildItem(
title: appLocalizations.expectedStatus,
leading: leading,
trailing: TextFormField(
textAlign: TextAlign.end,
trailing: FormRowTextField(
initialValue: expectedStatus,
inputFormatters: TextInputLimits.limit(TextInputLimits.status),
onChanged: (value) {
@@ -878,10 +862,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
),
);
},
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: appLocalizations.optional,
),
hintText: appLocalizations.optional,
),
);
}
@@ -931,7 +912,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
else
Text(
appLocalizations.optional,
style: context.listTitleStyle?.copyWith(
style: context.listTitleStyle.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
@@ -946,7 +927,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
return _buildItem(
invalid: invalid,
title: appLocalizations.name,
trailing: TextFormField(
trailing: FormRowTextField(
initialValue: name,
keyboardType: TextInputType.name,
inputFormatters: TextInputLimits.policyName(TextInputLimits.groupName),
@@ -958,11 +939,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
onFieldSubmitted: (_) {
_handleSave();
},
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: appLocalizations.inputProxyGroupName,
),
hintText: appLocalizations.inputProxyGroupName,
),
);
}
@@ -1066,11 +1043,25 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
}
Future<void> _handleDelete(int profileId) async {
final appLocalizations = context.appLocalizations;
final id = ref.read(proxyGroupProvider).id;
final inUse = await ref.read(profilesActionProvider.notifier).groupsInUse(
profileId,
{id},
);
if (!mounted) {
return;
}
final res = await dialogs.showMessage(
message: TextSpan(text: context.appLocalizations.confirmDeleteProxyGroup),
message: TextSpan(
text: inUse.isEmpty
? appLocalizations.confirmDeleteProxyGroup
: appLocalizations.proxyGroupInUse(inUse.join(', ')),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res == true && mounted) {
final id = ref.read(proxyGroupProvider).id;
ref.read(proxyGroupsProvider(profileId).notifier).delAll([id]);
context.safeNestedPop();
}
@@ -1350,11 +1341,10 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
.whereType<InvalidEmptyFallbackIssue>()
.toList();
return CommonScaffold(
iconActions: customFormActions(
context,
onQuickEdit: _handleQuickEdit,
onSave: _handleSave,
),
iconActions: [
customQuickEditAction(context, onPressed: _handleQuickEdit),
],
confirmationAction: customSaveAction(context, onPressed: _handleSave),
body: CustomScrollView(
key: ValueKey(_revision),
slivers: [
@@ -1363,7 +1353,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.list(
children: [
generateSectionV3(
generateSection(
title: appLocalizations.general,
items: [
_field(
@@ -1388,7 +1378,7 @@ class _EditProxyGroupViewState extends ConsumerState<EditProxyGroupView> {
sliver: SliverList.list(
children: [
_buildOptions(emptyFallbackIssues: emptyFallbackIssues),
generateSectionV3(
generateSection(
title: appLocalizations.action,
items: [
if (!_isNew)
+36 -30
View File
@@ -4,10 +4,10 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/database/database.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:material_ui/material_ui.dart';
@@ -202,6 +202,8 @@ class _IconPickerViewState extends ConsumerState<IconPickerView> {
message: TextSpan(
text: appLocalizations.deleteTip(_iconLabel(context, icon)),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res != true) {
return;
@@ -405,7 +407,6 @@ class _CurrentIcon extends StatelessWidget {
final description = _describe(context);
final subtitle = description.subtitle;
return CommonCard(
type: CommonCardType.filled,
radius: AppCorner.xl,
child: Padding(
padding: const EdgeInsets.all(12),
@@ -429,7 +430,7 @@ class _CurrentIcon extends StatelessWidget {
description.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyLarge,
style: AppTextStyle.body.of(context),
),
),
if (subtitle != null && subtitle.isNotEmpty)
@@ -437,9 +438,11 @@ class _CurrentIcon extends StatelessWidget {
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
style: AppTextStyle.footnote
.of(context)
.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
],
),
@@ -501,7 +504,7 @@ class SliverIconGrid extends StatelessWidget {
mainAxisExtent:
_iconPlateSize +
_iconLabelGap +
globalState.measure.bodySmallHeight,
globalState.measure.textHeight(AppTextStyle.footnote),
),
itemCount: icons.length,
itemBuilder: (context, index) {
@@ -544,40 +547,43 @@ class _IconTile extends StatelessWidget {
child: Column(
spacing: _iconLabelGap,
children: [
Material(
color: colorScheme.secondaryContainer,
shape: isSelected
? AppShape.md.copyWith(
side: BorderSide(color: colorScheme.primary, width: 2),
)
: AppShape.md,
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: AppShape.md,
onTap: onPressed,
onLongPress: onLongPress,
child: SizedBox.square(
dimension: _iconPlateSize,
child: Center(
Stack(
clipBehavior: Clip.none,
children: [
Material(
color: context.raisedSurfaceLevel.colorOf(colorScheme),
shape: AppShape.md,
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: AppShape.md,
onTap: onPressed,
onLongPress: onLongPress,
child: SizedBox.square(
dimension: _iconSize,
child: IconTheme.merge(
data: const IconThemeData(size: _iconSize),
child: CommonTargetIcon(src: icon.url),
dimension: _iconPlateSize,
child: Center(
child: SizedBox.square(
dimension: _iconSize,
child: IconTheme.merge(
data: const IconThemeData(size: _iconSize),
child: CommonTargetIcon(src: icon.url),
),
),
),
),
),
),
),
if (isSelected)
const Positioned(top: -4, right: -4, child: SelectionBadge()),
],
),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: context.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
style: AppTextStyle.footnote
.of(context)
.copyWith(color: colorScheme.onSurfaceVariant),
),
],
),
+80 -29
View File
@@ -2,7 +2,6 @@ import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart' hide FileInfo;
import 'package:fl_clash/providers/providers.dart';
@@ -13,9 +12,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
/// `proxies` names a group's proxies, `use` its providers, `use-position` where
/// those sit among the proxies, and `filter` what of the providers' proxies it
/// takes. `exclude-filter` drops whatever matches it, named members too.
/// The include-all keys take in only the local proxies and providers some group
/// of the profile names, since the config declares no others.
enum _MemberKind {
proxy,
provider,
allProxies,
allProviders,
filter,
excludeFilter;
@@ -24,7 +27,7 @@ enum _MemberKind {
_MemberKind get run => this == provider ? proxy : this;
(Color, Color) chipColorsOf(ColorScheme colorScheme) => switch (this) {
proxy || provider => (
proxy || provider || allProxies || allProviders => (
colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer,
),
@@ -46,18 +49,23 @@ enum _MemberKind {
provider => ref.watch(
customProfileProxyProviderIsValidProvider(profileId, name),
),
filter || excludeFilter => true,
allProxies || allProviders || filter || excludeFilter => true,
};
String invalidMessage(BuildContext context, String name) => switch (this) {
proxy => context.appLocalizations.invalidProxy(name),
provider => context.appLocalizations.invalidProxyProvider(name),
filter || excludeFilter => '',
allProxies || allProviders || filter || excludeFilter => '',
};
}
typedef _Member = ({_MemberKind kind, String name});
const _everyName = '*';
List<String> _everyOf(bool? includeAll) =>
includeAll == true ? const [_everyName] : const [];
/// The form state of type [M] whose [kinds] of members are listed.
abstract class _MemberHost<M> {
const _MemberHost();
@@ -166,6 +174,8 @@ class _GroupMembers extends _MemberHost<ProxyGroup> {
List<String> namesOf(ProxyGroup group, _MemberKind kind) => switch (kind) {
_MemberKind.proxy => group.proxies ?? const [],
_MemberKind.provider => group.use ?? const [],
_MemberKind.allProxies => _everyOf(group.includeAllProxies),
_MemberKind.allProviders => _everyOf(group.includeAllProviders),
_MemberKind.filter => filterParts(group.filter),
_MemberKind.excludeFilter => filterParts(group.excludeFilter),
};
@@ -178,6 +188,12 @@ class _GroupMembers extends _MemberHost<ProxyGroup> {
) => switch (kind) {
_MemberKind.proxy => group.copyWith(proxies: names.isEmpty ? null : names),
_MemberKind.provider => group.copyWith(use: names.isEmpty ? null : names),
_MemberKind.allProxies => group.copyWith(
includeAllProxies: names.isEmpty ? null : true,
),
_MemberKind.allProviders => group.copyWith(
includeAllProviders: names.isEmpty ? null : true,
),
_MemberKind.filter => group.copyWith(filter: joinFilterParts(names)),
_MemberKind.excludeFilter => group.copyWith(
excludeFilter: joinFilterParts(names),
@@ -465,7 +481,14 @@ class _MembersSliverState<M> extends ConsumerState<_MembersSliver<M>>
child: _MemberRow(
member: member,
index: index,
title: member.kind.isFilter ? presets[name] ?? name : name,
title: switch (member.kind) {
_MemberKind.allProxies => appLocalizations.allReferencedProxies,
_MemberKind.allProviders =>
appLocalizations.allReferencedProxyProviders,
_MemberKind.filter ||
_MemberKind.excludeFilter => presets[name] ?? name,
_MemberKind.proxy || _MemberKind.provider => name,
},
subtitle: switch (member.kind) {
_MemberKind.proxy =>
groupTypes[name] ??
@@ -474,6 +497,8 @@ class _MembersSliverState<M> extends ConsumerState<_MembersSliver<M>>
? appLocalizations.basicStrategy
: null),
_MemberKind.provider => appLocalizations.proxyProviders,
_MemberKind.allProxies ||
_MemberKind.allProviders => appLocalizations.autoInclude,
_MemberKind.filter => appLocalizations.filters,
_MemberKind.excludeFilter => appLocalizations.excludeFilter,
},
@@ -571,21 +596,24 @@ class _MemberRow extends ConsumerWidget {
}
}
typedef _Entry = ({String name, String title, String? subtitle});
typedef _Entry = ({
_MemberKind kind,
String name,
String title,
String? subtitle,
});
_Entry _named(String name, String? subtitle) =>
(name: name, title: name, subtitle: subtitle);
_Entry _named(_MemberKind kind, String name, String? subtitle) =>
(kind: kind, name: name, title: name, subtitle: subtitle);
_Entry _every(_MemberKind kind, String title) =>
(kind: kind, name: _everyName, title: title, subtitle: null);
class _Category {
final String label;
final _MemberKind kind;
final List<_Entry> entries;
const _Category({
required this.label,
required this.kind,
required this.entries,
});
const _Category({required this.label, required this.entries});
}
class _MemberPicker<M> extends ConsumerStatefulWidget {
@@ -646,7 +674,12 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
return [
for (final filter in filters)
if (!filter.isAppliedTo(value))
(name: filter.regex, title: filter.label, subtitle: filter.regex),
(
kind: kind,
name: filter.regex,
title: filter.label,
subtitle: filter.regex,
),
];
}
@@ -658,18 +691,19 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
if (kinds.contains(_MemberKind.filter))
_Category(
label: appLocalizations.filters,
kind: _MemberKind.filter,
entries: presetsMissingFrom(_MemberKind.filter),
),
if (kinds.contains(_MemberKind.excludeFilter))
_Category(
label: appLocalizations.excludeFilter,
kind: _MemberKind.excludeFilter,
entries: presetsMissingFrom(_MemberKind.excludeFilter),
),
];
}
bool _takesInEvery(_MemberKind kind) =>
_host.namesOf(_origin, kind).isNotEmpty;
List<_Category> _proxyCategories(AppLocalizations appLocalizations) {
final profileId = ProfileIdProvider.of(context)!.profileId;
final proxies = _host.takenIn(_origin, _MemberKind.proxy);
@@ -679,30 +713,47 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
),
);
final nodes = _nodeTypes(ref);
final hasProviders = ref
.watch(appProviderNamesProvider(ProviderKind.proxy))
.isNotEmpty;
return [
_Category(
label: appLocalizations.basicStrategy,
kind: _MemberKind.proxy,
entries: [
for (final target in RuleTarget.baseTargetNames)
if (!proxies.contains(target)) _named(target, null),
if (!proxies.contains(target))
_named(_MemberKind.proxy, target, null),
],
),
_Category(
label: appLocalizations.autoInclude,
entries: [
if (nodes.isNotEmpty && !_takesInEvery(_MemberKind.allProxies))
_every(
_MemberKind.allProxies,
appLocalizations.allReferencedProxies,
),
if (hasProviders && !_takesInEvery(_MemberKind.allProviders))
_every(
_MemberKind.allProviders,
appLocalizations.allReferencedProxyProviders,
),
],
),
_Category(
label: appLocalizations.localProxies,
kind: _MemberKind.proxy,
entries: [
for (final MapEntry(key: name, value: description) in nodes.entries)
if (!proxies.contains(name)) _named(name, description),
if (!proxies.contains(name))
_named(_MemberKind.proxy, name, description),
],
),
_Category(
label: appLocalizations.proxyGroup,
kind: _MemberKind.proxy,
entries: [
for (final item in groups.value)
if (!proxies.contains(item.name))
_named(item.name, item.type.value),
_named(_MemberKind.proxy, item.name, item.type.value),
],
),
];
@@ -716,10 +767,10 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
final providers = _providerTypes(ref, appLocalizations);
return _Category(
label: appLocalizations.proxyProviders,
kind: _MemberKind.provider,
entries: [
for (final name in providerNames)
if (!use.contains(name)) _named(name, providers[name]),
if (!use.contains(name))
_named(_MemberKind.provider, name, providers[name]),
],
);
}
@@ -731,7 +782,6 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
for (final category in _categories(appLocalizations))
_Category(
label: category.label,
kind: category.kind,
entries: category.entries
.whereMatches(_query, (entry) => [entry.title, entry.subtitle])
.toList(),
@@ -770,7 +820,7 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
initiallyExpanded: false,
itemCount: category.entries.length,
itemBuilder: (_, index) =>
_buildEntry(category.kind, category.entries[index]),
_buildEntry(category.entries[index]),
),
],
),
@@ -782,7 +832,8 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
);
}
Widget _buildEntry(_MemberKind kind, _Entry entry) {
Widget _buildEntry(_Entry entry) {
final kind = entry.kind;
final member = (kind: kind, name: entry.name);
final isSelected = _selected.contains(member);
final subtitle = entry.subtitle;
@@ -797,7 +848,7 @@ class _MemberPickerState<M> extends ConsumerState<_MemberPicker<M>> {
_ when kind.isFilter => FilterRegexText(subtitle),
_ => TooltipLabel(subtitle, maxLines: 1),
},
trailing: isSelected ? const GlyphIcon(AppGlyphs.check) : null,
trailing: isSelected ? const SelectionCheck() : null,
);
}
}
+14 -21
View File
@@ -3,10 +3,10 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/views/profiles/custom/members.dart';
import 'package:fl_clash/views/profiles/custom/quick_edit.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -66,7 +66,7 @@ class CustomProvidersView extends ConsumerWidget {
horizontal: 16,
).copyWith(top: context.contentTopPadding, bottom: 16),
children: [
generateSectionV3(
generateSection(
title: appLocalizations.proxies,
items: [
for (final name in proxy)
@@ -79,7 +79,7 @@ class CustomProvidersView extends ConsumerWidget {
),
],
),
generateSectionV3(
generateSection(
title: appLocalizations.rules,
items: [
for (final name in rule)
@@ -161,12 +161,13 @@ class _ProxyProviderItem extends ConsumerWidget {
? InfoMessageButton(
message: context.appLocalizations.invalidProxyProvider(name),
)
: CommonMinIconButtonTheme(
child: ElasticButton(
child: IconButton.filledTonal(
: TonalButtonTheme(
size: TonalButtonSize.compact,
child: ElasticPress(
child: IconButton(
tooltip: context.appLocalizations.edit,
onPressed: () => _handleEdit(context, ref),
icon: const GlyphIcon(AppGlyphs.edit, fill: 1),
icon: const GlyphIcon(AppGlyphs.edit),
),
),
),
@@ -407,15 +408,11 @@ class _ProxyProviderOptionsViewState
return FormRow(
title: option.label(context.appLocalizations),
leading: leading,
trailing: TextFormField(
textAlign: TextAlign.end,
trailing: FormRowTextField(
initialValue: value,
inputFormatters: TextInputLimits.limit(maxLength),
onChanged: (value) => onChanged(value.isEmpty ? null : value),
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: context.appLocalizations.optional,
),
hintText: context.appLocalizations.optional,
),
);
}
@@ -435,21 +432,17 @@ class _ProxyProviderOptionsViewState
spacing: 4,
children: [
Flexible(
child: TextFormField(
child: FormRowTextField(
keyboardType: TextInputType.number,
inputFormatters: TextInputLimits.digitsOnly(
TextInputLimits.number,
),
textAlign: TextAlign.end,
initialValue: value?.toString(),
onChanged: (value) => onChanged(int.tryParse(value)),
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: context.appLocalizations.optional,
),
hintText: context.appLocalizations.optional,
),
),
Text(suffix, style: context.textTheme.bodyMedium),
Text(suffix, style: AppTextStyle.subheadline.of(context)),
],
),
);
@@ -604,7 +597,7 @@ class _ProxyProviderOptionsViewState
void handleAdd() => _handleAddOption(remaining);
return CommonScaffold(
title: widget.name,
iconActions: customFormActions(context, onSave: _handleSave),
confirmationAction: customSaveAction(context, onPressed: _handleSave),
body: CustomScrollView(
slivers: [
SliverPadding(
+21 -20
View File
@@ -1,31 +1,31 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/models.dart' hide FileInfo;
import 'package:fl_clash/pages/editor.dart';
import 'package:fl_clash/widgets/sheet_navigator.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/ui/ui.dart';
import 'package:material_ui/material_ui.dart';
List<IconButtonData> customFormActions(
IconButtonData customQuickEditAction(
BuildContext context, {
VoidCallback? onQuickEdit,
required VoidCallback onSave,
required VoidCallback onPressed,
}) {
final appLocalizations = context.appLocalizations;
return [
if (onQuickEdit != null)
IconButtonData(
glyph: AppGlyphs.compose,
onPressed: onQuickEdit,
tooltip: appLocalizations.quickEdit,
),
IconButtonData(
glyph: AppGlyphs.check,
onPressed: onSave,
tooltip: appLocalizations.save,
),
];
return IconButtonData(
glyph: AppGlyphs.compose,
onPressed: onPressed,
tooltip: context.appLocalizations.quickEdit,
);
}
IconButtonData customSaveAction(
BuildContext context, {
required VoidCallback onPressed,
}) {
return IconButtonData(
glyph: AppGlyphs.check,
onPressed: onPressed,
tooltip: context.appLocalizations.save,
);
}
/// Thrown by an `apply` the user called off, which keeps the editor open.
@@ -65,6 +65,7 @@ Future<void> showCustomQuickEdit(
message: TextSpan(
text: '$message\n\n${currentAppLocalizations.discardChanges}',
),
destructive: true,
);
return res == true;
}
+27 -74
View File
@@ -1,10 +1,10 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/pages/editor.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -48,6 +48,8 @@ class _CustomRulesViewState extends ConsumerState<CustomRulesView> {
message: TextSpan(
text: appLocalizations.deleteTip(appLocalizations.rule),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res == true && mounted) {
_handleDelete({rule.id});
@@ -100,6 +102,9 @@ class _CustomRulesViewState extends ConsumerState<CustomRulesView> {
content,
previous: previous,
)) {
if (rule.ruleAction == RuleAction.SUB_RULE) {
continue;
}
final error = _ruleSaveError(context, rule);
if (error != null) {
throw MessageException(
@@ -139,7 +144,6 @@ class _CustomRulesViewState extends ConsumerState<CustomRulesView> {
final profileData = ref.watch(customProfileDataProvider(_profileId));
return ListEditorPage<Rule, int>(
title: appLocalizations.rule,
selectionEnabled: true,
idOf: (rule) => rule.id,
itemsOf: (ref) {
return ref.watch(profileRulesProvider(_profileId)).value;
@@ -167,7 +171,7 @@ class _CustomRulesViewState extends ConsumerState<CustomRulesView> {
onAdd: () => _handleAddOrUpdate(),
addMenuItems: [
CommonPopupMenuItem(
glyph: AppGlyphs.bolt,
glyph: AppGlyphs.rulePresets,
label: appLocalizations.quickActions,
onPressed: _handleQuickActions,
),
@@ -256,7 +260,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
action.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.listTitleStyle?.copyWith(
style: context.listTitleStyle.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
@@ -273,7 +277,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
final payloadError = ref.watch(
ruleProvider.select((state) => state.payloadError),
);
final field = TextFormField(
final field = FormRowTextField(
initialValue: content,
keyboardType: TextInputType.name,
inputFormatters: TextInputLimits.limit(TextInputLimits.rule),
@@ -282,11 +286,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
.read(ruleProvider.notifier)
.update((state) => state.copyWith(content: value));
},
textAlign: TextAlign.end,
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
hintText: appLocalizations.inputRuleContent,
),
hintText: appLocalizations.inputRuleContent,
);
return _buildItem(
invalid: payloadError != null,
@@ -317,7 +317,9 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
selectedOf: (ref) =>
ref.watch(ruleProvider.select((state) => state.ruleProvider)),
onSelected: (item) => Navigator.of(context).pop(item),
emptyLabel: context.appLocalizations.proxyProvidersEmpty,
emptyLabel: context.appLocalizations.nullTip(
context.appLocalizations.ruleProviders,
),
);
},
),
@@ -359,7 +361,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
ruleProvider ?? appLocalizations.selectRuleSet,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.listTitleStyle?.copyWith(color: foregroundColor),
style: context.listTitleStyle.copyWith(color: foregroundColor),
),
),
),
@@ -454,7 +456,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
target ?? appLocalizations.selectSplitStrategy,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.listTitleStyle?.copyWith(
style: context.listTitleStyle.copyWith(
color: foregroundColor,
),
),
@@ -469,56 +471,6 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
);
}
Future<void> _handleSelectedSubRule() async {
final res = await Navigator.of(context).push(
PagedSheetRoute(
builder: (context) => SelectionSheet<String>(
title: context.appLocalizations.subRule,
sections: const [SelectionSection(items: <String>[])],
labelBuilder: (item) => item,
selectedOf: (ref) =>
ref.watch(ruleProvider.select((state) => state.subRule)),
onSelected: (item) => Navigator.of(context).pop(item),
emptyLabel: context.appLocalizations.subRuleEmpty,
),
),
);
if (res == null) {
return;
}
ref
.read(ruleProvider.notifier)
.update((state) => state.copyWith(subRule: res));
}
Widget _buildSubRuleItem(String? subRule) {
final appLocalizations = context.appLocalizations;
return _buildItem(
title: appLocalizations.subRule,
onPressed: _handleSelectedSubRule,
trailing: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
subRule ?? appLocalizations.selectSubRule,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.listTitleStyle?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
),
),
const GlyphIcon(AppGlyphs.chevronForward),
],
),
);
}
Widget _buildNoResolveItem(bool noResolve, bool src) {
final appLocalizations = context.appLocalizations;
// The core turns no-resolve on with src, so the switch cannot disagree.
@@ -567,6 +519,8 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
message: TextSpan(
text: appLocalizations.deleteTip(appLocalizations.rule),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res == true && mounted) {
final id = ref.read(ruleProvider).id;
@@ -581,14 +535,14 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
final profileId = ProfileIdProvider.of(context)!.profileId;
final rule = ref.watch(ruleProvider);
return CommonScaffold(
iconActions: customFormActions(context, onSave: _handleSave),
confirmationAction: customSaveAction(context, onPressed: _handleSave),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(
horizontal: 16,
).copyWith(bottom: 20, top: context.contentTopPadding),
children: [
generateSectionV3(
generateSection(
title: appLocalizations.basicInfo,
items: [
_buildTypeItem(rule.ruleAction),
@@ -596,20 +550,18 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> {
rule.ruleAction == RuleAction.RULE_SET
? _buildRuleProviderItem(profileId, rule.ruleProvider)
: _buildContentItem(rule.content),
rule.ruleAction != RuleAction.SUB_RULE
? _buildTargetItem(profileId, rule.ruleTarget)
: _buildSubRuleItem(rule.subRule),
_buildTargetItem(profileId, rule.ruleTarget),
],
),
if (rule.ruleAction.hasParams)
generateSectionV3(
generateSection(
title: appLocalizations.additionalParameters,
items: [
_buildNoResolveItem(rule.noResolve, rule.src),
_buildSrcItem(rule.src),
],
),
generateSectionV3(
generateSection(
title: appLocalizations.action,
items: [
if (rule.id != -1)
@@ -648,6 +600,9 @@ String? _handleSaveRule(BuildContext context, WidgetRef ref) {
String? _ruleSaveError(BuildContext context, Rule rule) {
final appLocalizations = context.appLocalizations;
if (rule.ruleAction == RuleAction.SUB_RULE) {
return appLocalizations.invalidSubRule(rule.subRule ?? '');
}
final payloadError = rule.payloadError;
if (payloadError != null) {
return payloadError.getMessage(context);
@@ -655,13 +610,11 @@ String? _ruleSaveError(BuildContext context, Rule rule) {
if (rule.ruleAction != RuleAction.MATCH &&
rule.realContent?.isNotEmpty != true) {
return rule.ruleAction == RuleAction.RULE_SET
? appLocalizations.proxyProvidersNotEmpty
? appLocalizations.emptyTip(appLocalizations.ruleSet)
: appLocalizations.contentNotEmpty;
}
if (rule.realTarget?.isNotEmpty != true) {
return rule.ruleAction == RuleAction.SUB_RULE
? appLocalizations.subRuleNotEmpty
: appLocalizations.splitStrategyNotEmpty;
return appLocalizations.splitStrategyNotEmpty;
}
return null;
}
-1
View File
@@ -1 +0,0 @@
export 'package:fl_clash/features/form/form_row.dart' show InfoMessageButton;
+228 -143
View File
@@ -4,26 +4,25 @@ import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/pages/editor.dart';
import 'package:fl_clash/providers/action.dart';
import 'package:fl_clash/providers/core.dart';
import 'package:fl_clash/providers/database.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum _ProfileField { label, url, autoUpdateInterval }
class EditProfileView extends ConsumerStatefulWidget {
final Profile profile;
final BuildContext context;
const EditProfileView({
super.key,
required this.context,
required this.profile,
});
const EditProfileView({super.key, required this.profile});
@override
ConsumerState<EditProfileView> createState() => _EditProfileViewState();
@@ -36,7 +35,7 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
late final TextEditingController _autoUpdateDurationController;
late bool _autoUpdate;
String? _rawText;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool _attempted = false;
final _fileInfoNotifier = ValueNotifier<FileInfo?>(null);
late SetupAction _setupAction;
Uint8List? _fileData;
@@ -54,9 +53,7 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
text: widget.profile.autoUpdateDuration.inMinutes.toString(),
);
_setupAction = ref.read(setupActionProvider.notifier);
if (widget.profile.type != ProfileType.custom) {
_updateFileInfo();
}
_updateFileInfo();
}
Future<void> _updateFileInfo() async {
@@ -68,22 +65,83 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
_fileInfoNotifier.value = fileInfo;
}
({_ProfileField field, String message})? _issue(
AppLocalizations appLocalizations,
) {
final labelError = validateProfileName(
appLocalizations,
_labelController.text,
);
if (labelError != null) {
return (field: _ProfileField.label, message: labelError);
}
if (widget.profile.type != ProfileType.url) {
return null;
}
final url = _urlController.text;
if (url.isEmpty) {
return (
field: _ProfileField.url,
message: appLocalizations.profileUrlNullValidationDesc,
);
}
if (!url.isUrl) {
return (
field: _ProfileField.url,
message: appLocalizations.profileUrlInvalidValidationDesc,
);
}
if (!_autoUpdate) {
return null;
}
final interval = _autoUpdateDurationController.text;
if (interval.isEmpty) {
return (
field: _ProfileField.autoUpdateInterval,
message: appLocalizations.profileAutoUpdateIntervalNullValidationDesc,
);
}
if ((int.tryParse(interval) ?? 0) <= 0) {
return (
field: _ProfileField.autoUpdateInterval,
message:
appLocalizations.profileAutoUpdateIntervalInvalidValidationDesc,
);
}
return null;
}
void _handleChanged(String _) {
if (_attempted) {
setState(() {});
}
}
Future<void> _handleConfirm() async {
if (!_formKey.currentState!.validate()) return;
var profile = widget.profile.copyWith(
if (_issue(context.appLocalizations) != null) {
setState(() {
_attempted = true;
});
return;
}
final current =
ref.read(profilesProvider).getProfile(widget.profile.id) ??
widget.profile;
final interval = int.tryParse(_autoUpdateDurationController.text);
var profile = current.copyWith(
url: _urlController.text,
ageSecretKey: _ageSecretKeyController.text.trim(),
label: _labelController.text,
autoUpdate: _autoUpdate,
autoUpdateDuration: Duration(
minutes: int.parse(_autoUpdateDurationController.text),
),
autoUpdateDuration: interval != null && interval > 0
? Duration(minutes: interval)
: current.autoUpdateDuration,
);
final profilesAction = ref.read(profilesActionProvider.notifier);
final appLocalizations = context.appLocalizations;
final hasUpdate =
widget.profile.url != profile.url ||
widget.profile.ageSecretKey != profile.ageSecretKey;
current.url != profile.url ||
current.ageSecretKey != profile.ageSecretKey;
if (_fileData != null) {
if (profile.type == ProfileType.url && _autoUpdate) {
final res = await dialogs.showMessage(
@@ -179,11 +237,13 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
},
);
final data = await BaseNavigator.push<String>(context, editorPage);
if (data == null) {
if (data == null || !mounted) {
return;
}
_rawText = data;
_fileData = Uint8List.fromList(utf8.encode(data));
setState(() {
_fileData = Uint8List.fromList(utf8.encode(data));
});
_fileInfoNotifier.value = _fileInfoNotifier.value?.copyWith(
size: _fileData?.length ?? 0,
lastModified: DateTime.now(),
@@ -193,10 +253,13 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
Future<void> _uploadProfileFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
if (platformFile == null) return;
_fileData = await platformFile.readBytes();
final fileData = await platformFile.readBytes();
if (!mounted) {
return;
}
setState(() {
_fileData = fileData;
});
_fileInfoNotifier.value = _fileInfoNotifier.value?.copyWith(
size: _fileData?.length ?? 0,
lastModified: DateTime.now(),
@@ -232,64 +295,74 @@ class _EditProfileViewState extends ConsumerState<EditProfileView> {
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
final items = <Widget>[
_ProfileNameField(controller: _labelController),
if (widget.profile.type == ProfileType.url)
_ProfileUrlField(controller: _urlController),
if (widget.profile.type != ProfileType.custom)
_AgeSecretKeyField(controller: _ageSecretKeyController),
if (widget.profile.type == ProfileType.url) ...[
ListItem.toggle(
title: Text(appLocalizations.autoUpdate),
value: _autoUpdate,
onChanged: _setAutoUpdate,
),
if (_autoUpdate)
_AutoUpdateIntervalField(controller: _autoUpdateDurationController),
],
if (widget.profile.type != ProfileType.custom)
_ProfileFileItem(
fileInfoNotifier: _fileInfoNotifier,
onEdit: _editProfileFile,
onUpload: _uploadProfileFile,
final isUrl = widget.profile.type == ProfileType.url;
final issue = _attempted ? _issue(appLocalizations) : null;
final sections = [
generateSection(
items: [
_ProfileNameField(
controller: _labelController,
invalid: issue?.field == _ProfileField.label,
onChanged: _handleChanged,
),
if (isUrl)
_ProfileUrlField(
controller: _urlController,
invalid: issue?.field == _ProfileField.url,
onChanged: _handleChanged,
),
_AgeSecretKeyField(controller: _ageSecretKeyController),
],
),
if (isUrl)
generateSection(
items: [
FormRow(
title: appLocalizations.autoUpdate,
onPressed: () => _setAutoUpdate(!_autoUpdate),
trailing: Switch(value: _autoUpdate, onChanged: _setAutoUpdate),
),
if (_autoUpdate)
_AutoUpdateIntervalField(
controller: _autoUpdateDurationController,
invalid: issue?.field == _ProfileField.autoUpdateInterval,
onChanged: _handleChanged,
),
],
),
_ProfileFileItem(
fileInfoNotifier: _fileInfoNotifier,
onEdit: _editProfileFile,
onUpload: _uploadProfileFile,
),
];
return FocusTraversalGroup(
policy: PageTraversalPolicy(),
child: PageFocusScope(
child: CommonPopScope(
onPop: (context) {
if (_fileData == null) {
return true;
}
_handleBack();
return false;
},
onPop: _fileData == null
? null
: (_) {
_handleBack();
return false;
},
child: CommonScaffold(
title: appLocalizations.edit,
iconActions: [
IconButtonData(
glyph: AppGlyphs.check,
onPressed: _handleConfirm,
tooltip: appLocalizations.save,
),
],
body: Form(
key: _formKey,
child: Builder(
builder: (context) => ListView.separated(
padding: kMaterialListPadding.copyWith(
top: context.contentTopPadding,
bottom: BottomInsetScope.of(context) + 16,
),
itemBuilder: (_, index) {
return items[index];
},
separatorBuilder: (_, _) {
return const SizedBox(height: 24);
},
itemCount: items.length,
confirmationAction: IconButtonData(
glyph: AppGlyphs.check,
onPressed: _handleConfirm,
tooltip: appLocalizations.save,
),
body: Builder(
builder: (context) => ListView(
padding: const EdgeInsets.symmetric(horizontal: 16).copyWith(
top: context.contentTopPadding,
bottom: BottomInsetScope.of(context) + 16,
),
children: [
ErrorBanner(message: issue?.message),
...sections.separated(const SizedBox(height: 24)),
],
),
),
),
@@ -331,51 +404,59 @@ Future<void> renameProfile(
}
class _ProfileNameField extends StatelessWidget {
const _ProfileNameField({required this.controller});
const _ProfileNameField({
required this.controller,
required this.invalid,
required this.onChanged,
});
final TextEditingController controller;
final bool invalid;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
return ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
return FormRow(
title: appLocalizations.name,
invalid: invalid,
trailing: FormRowTextField(
controller: controller,
hintText: appLocalizations.required,
textInputAction: TextInputAction.next,
inputFormatters: TextInputLimits.limit(TextInputLimits.name),
decoration: InputDecoration(labelText: appLocalizations.name),
validator: (value) => validateProfileName(appLocalizations, value),
onChanged: onChanged,
),
);
}
}
class _ProfileUrlField extends StatelessWidget {
const _ProfileUrlField({required this.controller});
const _ProfileUrlField({
required this.controller,
required this.invalid,
required this.onChanged,
});
final TextEditingController controller;
final bool invalid;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
return ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.url,
return FormRow(
title: appLocalizations.url,
invalid: invalid,
trailing: FormRowTextField(
controller: controller,
hintText: appLocalizations.required,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.url),
maxLines: 5,
minLines: 1,
decoration: InputDecoration(labelText: appLocalizations.url),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.profileUrlNullValidationDesc;
}
if (!value.isUrl) {
return appLocalizations.profileUrlInvalidValidationDesc;
}
return null;
},
onChanged: onChanged,
),
);
}
@@ -389,48 +470,55 @@ class _AgeSecretKeyField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
return ListItem(
title: SecretTextFormField(
textInputAction: TextInputAction.next,
return FormRow(
title: appLocalizations.ageSecretKey,
trailing: FormRowSecretTextField(
controller: controller,
maxLines: 5,
decoration: InputDecoration(
labelText: appLocalizations.ageSecretKey,
hintText: appLocalizations.optional,
),
hintText: appLocalizations.optional,
textInputAction: TextInputAction.next,
),
);
}
}
class _AutoUpdateIntervalField extends StatelessWidget {
const _AutoUpdateIntervalField({required this.controller});
const _AutoUpdateIntervalField({
required this.controller,
required this.invalid,
required this.onChanged,
});
final TextEditingController controller;
final bool invalid;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
return ListItem(
title: TextFormField(
textInputAction: TextInputAction.next,
controller: controller,
inputFormatters: TextInputLimits.digitsOnly(TextInputLimits.interval),
decoration: InputDecoration(
labelText: appLocalizations.autoUpdateInterval,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.profileAutoUpdateIntervalNullValidationDesc;
}
try {
int.parse(value);
} catch (_) {
return appLocalizations
.profileAutoUpdateIntervalInvalidValidationDesc;
}
return null;
},
return FormRow(
title: appLocalizations.autoUpdateInterval,
invalid: invalid,
trailing: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: FormRowTextField(
controller: controller,
hintText: appLocalizations.required,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
inputFormatters: TextInputLimits.digitsOnly(
TextInputLimits.interval,
),
onChanged: onChanged,
),
),
Text(
appLocalizations.minutes,
style: AppTextStyle.subheadline.of(context),
),
],
),
);
}
@@ -482,28 +570,25 @@ class _ProfileFileItem extends StatelessWidget {
alignment: Alignment.centerLeft,
child: fileInfo == null
? Container()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ItemPositionProvider(
position: ItemPosition.startAndEnd,
child: ContextMenuRegion(
menuItems: _menuItems(context),
child: DecorationListItem(
contentPadding: const EdgeInsets.only(left: 16),
title: Text(appLocalizations.profile),
subtitle: _buildSubtitle(context, fileInfo),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
fileInfo.size.traffic.show,
style: context.listCaptionStyle,
),
const DisclosureIndicator(),
],
),
onPressed: onEdit,
: ItemPositionProvider(
position: ItemPosition.startAndEnd,
child: ContextMenuRegion(
menuItems: _menuItems(context),
child: DecorationListItem(
contentPadding: const EdgeInsets.only(left: 16),
title: Text(appLocalizations.profile),
subtitle: _buildSubtitle(context, fileInfo),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
fileInfo.size.traffic.show,
style: context.listCaptionStyle,
),
const DisclosureIndicator(),
],
),
onPressed: onEdit,
),
),
),
+7 -4
View File
@@ -1,7 +1,7 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -105,6 +105,7 @@ class _Title extends ConsumerWidget {
for (final type in ExtendType.values)
CommonCard(
isSelected: extendType == type,
fillsSelection: true,
onPressed: () {
_handleChangeType(ref, profileId, type);
},
@@ -129,9 +130,11 @@ class _Title extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_getDesc(context, extendType),
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceVariant.opacity80,
),
style: AppTextStyle.footnote
.of(context)
.copyWith(
color: context.colorScheme.onSurfaceVariant.opacity80,
),
),
),
],
+12 -40
View File
@@ -16,12 +16,6 @@ class ScriptContent extends ConsumerWidget {
});
}
void _handleRadioChange(WidgetRef ref, int profileId, int? scriptId) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(scriptId: scriptId);
});
}
@override
Widget build(BuildContext context, ref) {
final appLocalizations = context.appLocalizations;
@@ -44,24 +38,17 @@ class ScriptContent extends ConsumerWidget {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: RadioGroup<int>(
groupValue: scriptId,
onChanged: (value) {
_handleRadioChange(ref, profileId, value);
},
child: generateSectionV3(
items: [
for (final script in scripts)
_ScriptOption(
label: script.label,
value: script.id,
isSelected: script.id == scriptId,
onSelected: () {
_handleChange(ref, profileId, script.id);
},
),
],
),
child: generateSection(
items: [
for (final script in scripts)
_ScriptOption(
label: script.label,
isSelected: script.id == scriptId,
onSelected: () {
_handleChange(ref, profileId, script.id);
},
),
],
),
),
),
@@ -82,13 +69,11 @@ class ScriptContent extends ConsumerWidget {
class _ScriptOption extends StatelessWidget {
const _ScriptOption({
required this.label,
required this.value,
required this.isSelected,
required this.onSelected,
});
final String label;
final int value;
final bool isSelected;
final VoidCallback onSelected;
@@ -96,21 +81,8 @@ class _ScriptOption extends StatelessWidget {
Widget build(BuildContext context) {
return DecorationListItem(
isSelected: isSelected,
horizontalTitleGap: 8,
contentPadding: const EdgeInsets.symmetric(horizontal: 14),
leading: SizedBox(
width: 24,
height: 24,
child: ExcludeFocus(
child: Radio<int>(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
toggleable: true,
value: value,
),
),
),
title: Text(label),
trailing: isSelected ? const SelectionCheck() : null,
onPressed: onSelected,
);
}
+46 -67
View File
@@ -1,10 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/form/form.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/state.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/views/config/rules.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:material_ui/material_ui.dart';
@@ -22,8 +21,9 @@ class _StandardContentState extends ConsumerState<StandardContent> {
late int _profileId;
Future<void> _handleAddOrUpdate([Rule? rule]) async {
final res = await dialogs.showCommonDialog<Rule>(
child: AddOrEditRuleDialog(rule: rule),
final res = await showFormSheet<Rule>(
context: context,
builder: (_) => RuleForm(rule: rule),
);
if (res == null) {
return;
@@ -68,6 +68,8 @@ class _StandardContentState extends ConsumerState<StandardContent> {
message: TextSpan(
text: appLocalizations.deleteMultipTip(appLocalizations.rule),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res != true) {
return;
@@ -122,14 +124,12 @@ class _StandardContentState extends ConsumerState<StandardContent> {
final addedRules = ref.watch(profileRulesProvider(_profileId)).value ?? [];
final selectedRules = ref.watch(itemsProvider(_key));
return CommonPopScope(
onPop: (_) {
if (selectedRules.isNotEmpty) {
ref.read(itemsProvider(_key).notifier).value = {};
return false;
}
Navigator.of(context).pop();
return false;
},
onPop: selectedRules.isEmpty
? null
: (_) {
ref.read(itemsProvider(_key).notifier).value = {};
return false;
},
child: SliverMainAxisGroup(
slivers: [
const SliverToBoxAdapter(child: SizedBox(height: 24)),
@@ -139,56 +139,33 @@ class _StandardContentState extends ConsumerState<StandardContent> {
ListHeader(
title: appLocalizations.addedRules,
padding: EdgeInsets.symmetric(
horizontal: 16.mAp,
vertical: 8.mAp,
horizontal: 16.mAp(context),
vertical: 8.mAp(context),
),
actions: [
if (selectedRules.isNotEmpty) ...[
CommonMinIconButtonTheme(
child: ElasticButton(
child: IconButton.filledTonal(
tooltip: context.appLocalizations.delete,
onPressed: () {
_handleDelete();
},
icon: const GlyphIcon(AppGlyphs.delete, fill: 1),
),
),
IconButtonData(
glyph: AppGlyphs.delete,
tooltip: appLocalizations.delete,
onPressed: _handleDelete,
),
IconButtonData(
glyph: AppGlyphs.selectAll,
tooltip: appLocalizations.selectAll,
onPressed: _handleSelectAll,
),
] else ...[
IconButtonData(
glyph: AppGlyphs.rulePresets,
tooltip: appLocalizations.quickActions,
onPressed: _handleQuickActions,
),
IconButtonData(
glyph: AppGlyphs.addCircle,
tooltip: appLocalizations.add,
onPressed: _handleAddOrUpdate,
),
],
if (selectedRules.isEmpty)
CommonMinIconButtonTheme(
child: ElasticButton(
child: IconButton.filledTonal(
tooltip: appLocalizations.quickActions,
onPressed: _handleQuickActions,
icon: const GlyphIcon(AppGlyphs.bolt, fill: 1),
),
),
),
if (selectedRules.isNotEmpty)
CommonMinFilledButtonTheme(
child: ElasticButton(
child: FilledButton(
onPressed: () {
_handleSelectAll();
},
child: Text(appLocalizations.selectAll),
),
),
)
else
CommonMinIconButtonTheme(
child: ElasticButton(
child: IconButton.filledTonal(
tooltip: appLocalizations.add,
onPressed: () {
_handleAddOrUpdate();
},
icon: const GlyphIcon(AppGlyphs.addCircle, fill: 1),
),
),
),
],
),
],
@@ -300,11 +277,14 @@ class _MatchTargetItem extends ConsumerWidget {
label: appLocalizations.matchTarget,
trailing: Text(
matchTarget ?? appLocalizations.followProfile,
style: context.textTheme.bodyMedium?.toJetBrainsMono.copyWith(
color: invalid
? context.colorScheme.error
: context.colorScheme.tertiary,
),
style: AppTextStyle.subheadline
.of(context)
.toJetBrainsMono
.copyWith(
color: invalid
? context.colorScheme.error
: context.colorScheme.tertiary,
),
),
onPressed: () {
_handleSelect(context, ref);
@@ -369,11 +349,10 @@ class _EditGlobalAddedRulesState extends ConsumerState<_EditGlobalAddedRules> {
nullStatus: NullStatus(
label: appLocalizations.nullTip(appLocalizations.rule),
illustration: NullStatusIllustration.rules,
action: ElasticButton(
child: FilledButton.tonal(
onPressed: _handleToAddedRules,
child: Text(appLocalizations.goToConfigureAddedRules),
),
action: TonalButton(
size: TonalButtonSize.bar,
label: appLocalizations.goToConfigureAddedRules,
onPressed: _handleToAddedRules,
),
),
child: ScrollConfiguration(
+104 -114
View File
@@ -1,12 +1,11 @@
import 'dart:async';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/ui/icons/icons.dart';
import 'package:fl_clash/views/profiles/custom/custom.dart';
import 'package:fl_clash/views/profiles/extend/extend.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -22,29 +21,25 @@ const _infoGap = 4.0;
double get _profileItemHeight {
final measure = globalState.measure;
return max(
SourceIcon.size,
measure.titleMediumHeight + _infoGap + measure.bodySmallHeight,
) +
return measure.textHeight(AppTextStyle.headline) +
_infoGap +
measure.textHeight(AppTextStyle.footnote) +
2 * listRowVerticalPadding;
}
class ProfilesView extends ConsumerStatefulWidget {
const ProfilesView({super.key});
const ProfilesView({super.key, this.picker = false});
/// Clears the bar and opens at the top, for switching profiles from a sheet.
final bool picker;
@override
ConsumerState<ProfilesView> createState() => _ProfilesViewState();
}
class _ProfilesViewState extends ConsumerState<ProfilesView> {
Function? applyConfigDebounce;
bool _isUpdating = false;
@override
void initState() {
super.initState();
}
Future<void> _updateProfiles(List<Profile> profiles) async {
if (_isUpdating == true) {
return;
@@ -104,18 +99,19 @@ class _ProfilesViewState extends ConsumerState<ProfilesView> {
final appLocalizations = context.appLocalizations;
final isLoading = ref.watch(loadingProvider(LoadingTag.profiles));
final state = ref.watch(profilesStateProvider);
final spacing = cardSpacing;
final spacing = cardSpacingOf(context, ref.watch(viewModeProvider));
final picker = widget.picker;
return CommonScaffold(
isLoading: isLoading,
title: appLocalizations.profiles,
primaryAction: state.profiles.isEmpty
primaryAction: state.profiles.isEmpty || picker
? null
: IconButtonData(
glyph: AppGlyphs.addCircle,
onPressed: () => showAddProfilePage(context),
tooltip: appLocalizations.addProfile,
),
iconActions: _buildActions(state.profiles),
iconActions: picker ? const [] : _buildActions(state.profiles),
foldPrimaryAction: true,
body: NullStatusSwitcher(
isEmpty: state.profiles.isEmpty,
@@ -123,18 +119,18 @@ class _ProfilesViewState extends ConsumerState<ProfilesView> {
label: appLocalizations.nullTip(appLocalizations.profiles),
description: appLocalizations.nullProfileDesc,
illustration: NullStatusIllustration.profile,
action: ElasticButton(
child: FilledButton.tonalIcon(
onPressed: () => showAddProfilePage(context),
icon: const GlyphIcon(AppGlyphs.addCircle, fill: 1),
label: Text(appLocalizations.addProfile),
),
action: TonalButton(
size: TonalButtonSize.bar,
glyph: AppGlyphs.addCircle,
label: appLocalizations.addProfile,
onPressed: () => showAddProfilePage(context),
),
),
child: _ProfilesGrid(
profiles: state.profiles,
currentProfileId: state.currentProfileId,
spacing: spacing,
revealsSelected: !picker,
),
),
);
@@ -148,11 +144,13 @@ class _ProfilesGrid extends ConsumerStatefulWidget {
required this.profiles,
required this.currentProfileId,
required this.spacing,
required this.revealsSelected,
});
final List<Profile> profiles;
final int? currentProfileId;
final double spacing;
final bool revealsSelected;
@override
ConsumerState<_ProfilesGrid> createState() => _ProfilesGridState();
@@ -173,7 +171,9 @@ class _ProfilesGridState extends ConsumerState<_ProfilesGrid> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_revealed || !PageActivityScope.isActiveOf(context)) {
if (_revealed ||
!widget.revealsSelected ||
!PageActivityScope.isActiveOf(context)) {
return;
}
_revealed = true;
@@ -196,7 +196,7 @@ class _ProfilesGridState extends ConsumerState<_ProfilesGrid> {
int _columnsFor(double width) => getProfilesColumns(
width - _horizontalPadding * 2,
spacing: widget.spacing,
minItemWidth: profileItemMinWidth.ap,
minItemWidth: profileItemMinWidth.ap(context),
);
void _revealSelected() {
@@ -230,14 +230,14 @@ class _ProfilesGridState extends ConsumerState<_ProfilesGrid> {
@override
Widget build(BuildContext context) {
final profiles = widget.profiles;
return LayoutBuilder(
builder: (_, constraints) {
return WidthBuilder(
builder: (_, width) {
return GridView.builder(
key: profilesStoreKey,
controller: _controller,
padding: _padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _columnsFor(constraints.maxWidth),
crossAxisCount: _columnsFor(width),
mainAxisSpacing: widget.spacing,
crossAxisSpacing: widget.spacing,
mainAxisExtent: _profileItemHeight,
@@ -286,6 +286,8 @@ class ProfileItem extends ConsumerWidget {
users.map((item) => item.realLabel).join(', '),
),
),
destructive: true,
confirmText: appLocalizations.delete,
);
if (res != true) {
return;
@@ -299,34 +301,6 @@ class ProfileItem extends ConsumerWidget {
);
}
void _handleShowSubscriptionInfo(BuildContext context) {
unawaited(
dialogs.showCommonDialog<void>(
context: context,
child: Builder(
builder: (context) {
return CommonDialog(
backgroundColor: context.colorScheme.surfaceContainerLow,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
title: context.appLocalizations.subscriptionInfo,
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.appLocalizations.confirm),
),
],
child: SubscriptionInfoDetailView(
subscriptionInfo: profile.subscriptionInfo!,
),
);
},
),
),
);
}
Future updateProfile(WidgetRef ref) async {
if (profile.type != ProfileType.url) return;
await globalState.loadingRun(() async {
@@ -339,23 +313,25 @@ class ProfileItem extends ConsumerWidget {
void _handleShowEditExtendPage(BuildContext context) {
showExtend(
context,
builder: (context) => EditProfileView(profile: profile, context: context),
builder: (context) => EditProfileView(profile: profile),
);
}
SubscriptionInfo? get _subscriptionInfo =>
profile.type == ProfileType.url ? profile.subscriptionInfo : null;
Widget? _buildTraffic(BuildContext context) {
String _subtitle(BuildContext context) {
final info = _subscriptionInfo;
if (info?.usage == null) {
return null;
if (info != null && info.usage != null) {
final used = info.upload + info.download;
return '${used.traffic.show} / ${info.total.traffic.show}';
}
final used = info!.upload + info.download;
return Text(
'${used.traffic.show} / ${info.total.traffic.show}',
style: context.textTheme.bodySmall?.toLighter,
);
final appLocalizations = context.appLocalizations;
return switch (profile.type) {
ProfileType.url => appLocalizations.remote,
ProfileType.file => appLocalizations.local,
ProfileType.custom => appLocalizations.custom,
};
}
Widget _buildTrailing(BuildContext context, {required bool isUpdating}) {
@@ -366,15 +342,11 @@ class ProfileItem extends ConsumerWidget {
children: [...previousChildren, ?currentChild],
),
child: isUpdating
? const SizedBox.square(
key: ValueKey('updating'),
dimension: 24,
child: CommonCircleLoading(),
)
? const CommonCircleLoading(key: ValueKey('updating'), size: 24)
: LastUpdateTimeText(
key: const ValueKey('lastUpdate'),
lastUpdateDate: profile.lastUpdateDate,
style: context.textTheme.bodySmall?.toLighter,
style: AppTextStyle.footnote.of(context).toLighter,
compact: true,
),
);
@@ -395,7 +367,7 @@ class ProfileItem extends ConsumerWidget {
final res = await globalState.safeRun<bool>(() async {
final mFile = await profile.file;
final value = await picker.saveFile(
profile.realLabel,
profile.exportFileName,
mFile.readAsBytesSync(),
);
if (value == null) return false;
@@ -445,7 +417,7 @@ class ProfileItem extends ConsumerWidget {
),
CommonPopupMenuItem(
glyph: AppGlyphs.eye,
label: appLocalizations.finalConfig,
label: appLocalizations.runtimeConfig,
onPressed: () {
_handlePreview(context);
},
@@ -473,7 +445,7 @@ class ProfileItem extends ConsumerWidget {
subItems: [
CommonPopupMenuItem(
glyph: AppGlyphs.eye,
label: appLocalizations.finalConfig,
label: appLocalizations.runtimeConfig,
onPressed: () {
_handlePreview(context);
},
@@ -510,39 +482,35 @@ class ProfileItem extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isUpdating = ref.watch(isUpdatingProvider(profile.updatingKey));
final subscriptionInfo = _subscriptionInfo;
final usage = subscriptionInfo?.usage;
final isSelected = profile.id == groupValue;
return ContextMenuRegion(
menuItems: isUpdating ? null : _menuItems(context, ref),
child: CommonCard(
enterActionsOnRight: true,
radius: AppCorner.xl,
isSelected: profile.id == groupValue,
isSelected: isSelected,
onPressed: () {
onChanged(profile.id);
},
child: ListItem(
key: Key(profile.id.toString()),
horizontalTitleGap: 12,
minTileHeight: _profileItemHeight,
minVerticalPadding: listRowVerticalPadding,
padding: const EdgeInsets.only(left: SourceIcon.inset, right: 16),
leading: SourceIcon(
kind: switch (profile.type) {
ProfileType.url => SourceKind.remote,
ProfileType.file => SourceKind.file,
ProfileType.custom => SourceKind.custom,
},
usage: usage,
tooltip: context.appLocalizations.subscriptionInfo,
onPressed: usage == null
? null
: () => _handleShowSubscriptionInfo(context),
trailing: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
if (profile.type == ProfileType.custom)
_GroupsCountChip(profileId: profile.id)
else
_buildTrailing(context, isUpdating: isUpdating),
if (isSelected) const SelectionBadge(size: 24),
],
),
trailing: _buildTrailing(context, isUpdating: isUpdating),
title: _ProfileCardTitle(
profile: profile,
expire: subscriptionInfo?.expireDate,
info: _buildTraffic(context),
subtitle: _subtitle(context),
),
),
),
@@ -554,40 +522,60 @@ class _ProfileCardTitle extends StatelessWidget {
const _ProfileCardTitle({
required this.profile,
required this.expire,
required this.info,
required this.subtitle,
});
final Profile profile;
final DateTime? expire;
final Widget? info;
final String subtitle;
@override
Widget build(BuildContext context) {
final title = ChipTitle(
title: profile.realLabel,
expire: expire,
style: context.textTheme.titleMedium,
);
final info = this.info;
if (info == null) {
return title;
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
ChipTitle(
title: profile.realLabel,
expire: expire,
style: AppTextStyle.headline.of(context),
),
const SizedBox(height: _infoGap),
DefaultTextStyle.merge(
Text(
subtitle,
style: AppTextStyle.footnote.of(context).toLighter,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: info,
),
],
);
}
}
/// Square-cornered so a lone digit never reads as a second [SelectionBadge].
class _GroupsCountChip extends ConsumerWidget {
const _GroupsCountChip({required this.profileId});
final int profileId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(profileGroupsCountProvider(profileId)).value;
if (count == null) {
return const SizedBox.shrink();
}
final colorScheme = context.colorScheme;
return Tooltip(
message: context.appLocalizations.proxyGroupsCount(count, count.compact),
child: TonalChip(
label: count.compact,
color: colorScheme.primary.opacity15,
foregroundColor: colorScheme.primary,
),
);
}
}
class ReorderableProfilesSheet extends ConsumerStatefulWidget {
final List<Profile> profiles;
@@ -624,22 +612,24 @@ class _ReorderableProfilesSheetState
void _handleSave() {
Navigator.of(context).pop();
ref.read(profilesProvider.notifier).reorder(profiles);
final latest = ref.read(profilesProvider);
final ids = {for (final profile in profiles) profile.id};
ref.read(profilesProvider.notifier).reorder([
for (final profile in profiles) ?latest.getProfile(profile.id),
for (final profile in latest)
if (!ids.contains(profile.id)) profile,
]);
}
@override
Widget build(BuildContext context) {
final appLocalizations = context.appLocalizations;
return CommonScaffold(
actions: [
AppBarActionButton(
data: IconButtonData(
glyph: AppGlyphs.check,
onPressed: _handleSave,
tooltip: context.appLocalizations.save,
),
),
],
confirmationAction: IconButtonData(
glyph: AppGlyphs.check,
onPressed: _handleSave,
tooltip: context.appLocalizations.save,
),
body: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: ReorderableListView.builder(
+10 -9
View File
@@ -35,8 +35,7 @@ class ResourcesView extends ConsumerStatefulWidget {
ConsumerState<ResourcesView> createState() => _ResourcesViewState();
}
class _ResourcesViewState extends ConsumerState<ResourcesView>
with RouteMotionHoldMixin<ResourcesView>, RouteSettledMixin<ResourcesView> {
class _ResourcesViewState extends ConsumerState<ResourcesView> {
_RemoteResources? _resources;
var _fileInfos = const <String, FileInfo?>{};
@@ -46,11 +45,9 @@ class _ResourcesViewState extends ConsumerState<ResourcesView>
ref.listenManual(iconSetsProvider, (_, _) => _refreshResources());
ref.listenManual(clashProvidersProvider, (_, _) => _refreshResources());
ref.listenManual(scriptsProvider, (_, _) => _refreshResources());
unawaited(_load());
}
@override
void didSettleRoute() => unawaited(_load());
Future<void> _load() async {
await Future.wait<Object>([
ref.read(iconSetsProvider.future),
@@ -101,7 +98,7 @@ class _ResourcesViewState extends ConsumerState<ResourcesView>
if (_resources == null) {
return;
}
updateWhenRouteSettled(() => setState(() => _resources = _readResources()));
setState(() => _resources = _readResources());
}
Future<void> _updateAll() async {
@@ -120,6 +117,9 @@ class _ResourcesViewState extends ConsumerState<ResourcesView>
value: updateInterval.toString(),
suffixText: appLocalizations.hours,
keyboardType: TextInputType.number,
inputFormatters: TextInputLimits.digitsOnly(
TextInputLimits.geoUpdateInterval,
),
validator: (value) {
if (value == null || value.isEmpty) {
return appLocalizations.emptyTip(
@@ -206,9 +206,10 @@ class _ResourcesViewState extends ConsumerState<ResourcesView>
),
],
body: NullStatusSwitcher(
isEmpty: resources == null,
holdsArrival: true,
nullStatus: const NullStatus(illustration: NullStatusIllustration.data),
isLoading: resources == null,
loadingStatus: const NullStatus(
illustration: NullStatusIllustration.data,
),
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
-320
View File
@@ -1,320 +0,0 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/icons/icons.dart';
import 'package:material_ui/material_ui.dart';
enum SourceKind { remote, file, custom }
extension on SourceKind {
Color tintOf(ColorScheme colorScheme) => switch (this) {
SourceKind.remote => colorScheme.primary,
SourceKind.file => colorScheme.tertiary,
SourceKind.custom => colorScheme.secondary,
};
}
/// The tile sits [inset] from the edge of an [AppCorner.xl] surface, so its
/// corner is concentric with the surface's.
const _sourceIconInset = 12.0;
const _sourceIconSize = 40.0;
final _sourceIconShape = AppShape.all(AppCorner.xl - _sourceIconInset);
class SourceIcon extends StatelessWidget {
const SourceIcon({
super.key,
required this.kind,
this.usage,
this.tooltip,
this.onPressed,
});
static const size = _sourceIconSize;
static const inset = _sourceIconInset;
final SourceKind kind;
final double? usage;
final String? tooltip;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final colorScheme = context.colorScheme;
final usage = this.usage;
final icon = Container(
width: size,
height: size,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: kind.tintOf(colorScheme).opacity15,
shape: _sourceIconShape,
),
child: TweenAnimationBuilder<double>(
tween: Tween(end: usage ?? 0),
duration: commonDuration,
curve: Curves.easeOutCubic,
builder: (_, level, _) => CustomPaint(
painter: _SourceArtPainter(
kind: kind,
colorScheme: colorScheme,
level: usage == null ? null : level,
),
),
),
);
if (onPressed == null) {
return icon;
}
return IconButton(
tooltip: tooltip,
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size.square(size),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: _sourceIconShape,
),
onPressed: onPressed,
icon: icon,
);
}
}
class _SourceArtPainter extends CustomPainter {
const _SourceArtPainter({
required this.kind,
required this.colorScheme,
required this.level,
});
static const _waveAmplitude = 1.2;
static const _cloudBounds = Rect.fromLTRB(5.5, 9.5, 34.5, 30);
static final _cloud = _cloudPath(_cloudBounds);
static const _badgeCenter = Offset(30.5, 27);
static final _badgedCloud = Path.combine(
PathOperation.difference,
_cloud,
Path()..addOval(Rect.fromCircle(center: _badgeCenter, radius: 7.4)),
);
final SourceKind kind;
final ColorScheme colorScheme;
final double? level;
@override
void paint(Canvas canvas, Size size) {
canvas.scale(size.width / _sourceIconSize);
switch (kind) {
case SourceKind.remote:
_paintRemote(canvas);
case SourceKind.file:
_paintFile(canvas);
case SourceKind.custom:
_paintCustom(canvas);
}
}
static Path _cloudPath(Rect bounds) {
final width = bounds.width;
final left = bounds.left;
final bottom = bounds.bottom;
final leftLobe = width * 0.25;
final rightLobe = width * 0.21;
final top = bounds.height - width * 0.33;
return Path.combine(
PathOperation.union,
Path.combine(
PathOperation.union,
Path()
..addOval(
Rect.fromCircle(
center: Offset(left + leftLobe, bottom - leftLobe),
radius: leftLobe,
),
)
..addOval(
Rect.fromCircle(
center: Offset(left + width - rightLobe, bottom - rightLobe),
radius: rightLobe,
),
),
Path()..addOval(
Rect.fromCircle(
center: Offset(left + width * 0.53, bounds.top + width * 0.33),
radius: width * 0.33,
),
),
),
Path()..addRect(
Rect.fromLTRB(
left + leftLobe,
bottom - top,
left + width - rightLobe,
bottom,
),
),
);
}
void _paintRemote(Canvas canvas) {
final primary = colorScheme.primary;
final tertiary = colorScheme.tertiary;
final level = this.level;
// A solid cloud is what a gauge with nothing used looks like.
if (level == null) {
canvas.drawPath(_badgedCloud, Paint()..color = primary);
_paintGlyph(canvas, AppGlyphs.removeCircle, _badgeCenter, 14, tertiary);
return;
}
canvas
..drawPath(_cloud, Paint()..color = primary.withValues(alpha: 0.32))
..save()
..clipPath(_cloud);
_paintWater(canvas, _cloudBounds, 1 - level, primary);
canvas.restore();
}
void _paintFile(Canvas canvas) {
final tertiary = colorScheme.tertiary;
final ink = Color.lerp(tertiary, colorScheme.onTertiary, 0.5)!;
canvas
..save()
..translate(16.5, 20.5)
..rotate(-pi / 24)
..drawRSuperellipse(
RSuperellipse.fromRectAndRadius(
Rect.fromCenter(center: Offset.zero, width: 14, height: 20.5),
const Radius.circular(3.5),
),
Paint()..color = colorScheme.secondary.withValues(alpha: 0.38),
)
..restore();
const page = Rect.fromLTRB(15, 8.5, 30, 31);
const fold = 6.0;
final corner = Path()
..moveTo(page.right - fold, page.top)
..lineTo(page.right, page.top)
..lineTo(page.right, page.top + fold)
..close();
canvas
..save()
..clipPath(
Path()
..fillType = PathFillType.evenOdd
..addRect(page)
..addPath(corner, Offset.zero),
)
..drawRSuperellipse(
RSuperellipse.fromRectAndRadius(page, const Radius.circular(3.5)),
Paint()..color = tertiary,
)
..restore()
..drawPath(
Path()
..moveTo(page.right - fold, page.top)
..lineTo(page.right - fold, page.top + fold - 2)
..quadraticBezierTo(
page.right - fold,
page.top + fold,
page.right - fold + 2,
page.top + fold,
)
..lineTo(page.right, page.top + fold)
..close(),
Paint()..color = ink,
);
for (final (top, right) in const [(19.5, 26.5), (24.0, 23.5)]) {
canvas.drawRSuperellipse(
RSuperellipse.fromLTRBR(
18.5,
top,
right,
top + 2.2,
const Radius.circular(1.1),
),
Paint()..color = ink,
);
}
}
void _paintCustom(Canvas canvas) {
const block = 11.5;
const gap = 3.0;
const start = (_sourceIconSize - block * 2 - gap) / 2;
const end = start + block + gap;
for (final (left, top, color) in [
(start, start, colorScheme.primary),
(start, end, colorScheme.tertiary),
(end, end, colorScheme.secondary),
]) {
canvas.drawRSuperellipse(
RSuperellipse.fromLTRBR(
left,
top,
left + block,
top + block,
const Radius.circular(3.5),
),
Paint()..color = color,
);
}
final plus = Paint()
..color = colorScheme.primary
..strokeWidth = 3
..strokeCap = StrokeCap.round;
const center = Offset(end + block / 2, start + block / 2);
const arm = block / 2 - 1.5;
canvas
..drawLine(center.translate(-arm, 0), center.translate(arm, 0), plus)
..drawLine(center.translate(0, -arm), center.translate(0, arm), plus);
}
void _paintGlyph(
Canvas canvas,
Glyph glyph,
Offset center,
double extent,
Color color,
) {
canvas
..save()
..translate(center.dx - extent / 2, center.dy - extent / 2);
GlyphPainter(
glyph: glyph,
fill: 1,
color: color,
).paint(canvas, Size.square(extent));
canvas.restore();
}
void _paintWater(Canvas canvas, Rect bounds, double level, Color color) {
final surface = bounds.bottom - bounds.height * level;
final amplitude = _waveAmplitude * sin(pi * level);
final quarter = bounds.width / 4;
canvas.drawPath(
Path()
..moveTo(bounds.left, surface)
..quadraticBezierTo(
bounds.left + quarter,
surface - amplitude * 2,
bounds.left + quarter * 2,
surface,
)
..quadraticBezierTo(
bounds.left + quarter * 3,
surface + amplitude * 2,
bounds.right,
surface,
)
..lineTo(bounds.right, bounds.bottom)
..lineTo(bounds.left, bounds.bottom)
..close(),
Paint()..color = color,
);
}
@override
bool shouldRepaint(_SourceArtPainter oldDelegate) =>
oldDelegate.kind != kind ||
oldDelegate.colorScheme != colorScheme ||
oldDelegate.level != level;
}