feat(profiles): manage external resources from the profile details
Changelog: Profile-detail resources and include-all in custom groups
This commit is contained in:
+3
-1
@@ -922,5 +922,7 @@
|
||||
"done": "Done",
|
||||
"dashboardAddableSection": "Available to add",
|
||||
"customImportDroppedSubRules": "{count, plural, =1{1 SUB-RULE rule was dropped, since custom profiles do not support sub-rules} other{{label} SUB-RULE rules were dropped, since custom profiles do not support sub-rules}}",
|
||||
"customImportDroppedIncludeAll": "Dropped include-all from the proxy groups {groups}; add the proxies or proxy providers they use"
|
||||
"autoInclude": "Auto-include",
|
||||
"allReferencedProxies": "All referenced proxies",
|
||||
"allReferencedProxyProviders": "All referenced proxy providers"
|
||||
}
|
||||
|
||||
+3
-1
@@ -922,5 +922,7 @@
|
||||
"done": "完了",
|
||||
"dashboardAddableSection": "追加できるカード",
|
||||
"customImportDroppedSubRules": "{count, plural, other{カスタム設定はサブルールに対応していないため、SUB-RULE ルール {label} 件を破棄しました}}",
|
||||
"customImportDroppedIncludeAll": "プロキシグループ {groups} の include-all を破棄しました。これらのグループで使うプロキシまたはプロキシプロバイダーを追加してください"
|
||||
"autoInclude": "自動で含める",
|
||||
"allReferencedProxies": "参照されているすべてのプロキシ",
|
||||
"allReferencedProxyProviders": "参照されているすべてのプロキシプロバイダー"
|
||||
}
|
||||
|
||||
+3
-1
@@ -922,5 +922,7 @@
|
||||
"done": "Готово",
|
||||
"dashboardAddableSection": "Можно добавить",
|
||||
"customImportDroppedSubRules": "{count, plural, one{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правило SUB-RULE} few{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правила SUB-RULE} many{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правил SUB-RULE} other{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правила SUB-RULE}}",
|
||||
"customImportDroppedIncludeAll": "У групп прокси {groups} отброшен include-all; добавьте прокси или провайдеры прокси, которые они используют"
|
||||
"autoInclude": "Автоматическое включение",
|
||||
"allReferencedProxies": "Все используемые прокси",
|
||||
"allReferencedProxyProviders": "Все используемые провайдеры прокси"
|
||||
}
|
||||
|
||||
+3
-1
@@ -922,5 +922,7 @@
|
||||
"done": "完成",
|
||||
"dashboardAddableSection": "可添加",
|
||||
"customImportDroppedSubRules": "{count, plural, other{自定义配置不支持子规则,已丢弃 {label} 条 SUB-RULE 规则}}",
|
||||
"customImportDroppedIncludeAll": "已丢弃策略组 {groups} 的 include-all,请为这些策略组添加代理或代理集"
|
||||
"autoInclude": "自动包含",
|
||||
"allReferencedProxies": "全部被引用的代理",
|
||||
"allReferencedProxyProviders": "全部被引用的代理集"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
@@ -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: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export 'package:fl_clash/features/form/form_row.dart' show InfoMessageButton;
|
||||
+228
-143
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -337,7 +337,6 @@ void main() {
|
||||
hashKey: 'in-user',
|
||||
defaultSelected: 'Old',
|
||||
emptyFallback: 'Old',
|
||||
includeAll: true,
|
||||
includeAllProxies: false,
|
||||
includeAllProviders: true,
|
||||
hidden: false,
|
||||
@@ -359,13 +358,18 @@ void main() {
|
||||
|
||||
final groups = await database.proxyGroupsDao.query(profile.id).get();
|
||||
expect(groups.first, first.copyWith(profileId: profile.id));
|
||||
expect(
|
||||
await database.proxyGroupsDao
|
||||
.profileGroupsCount(profile.id)
|
||||
.getSingle(),
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
(await (database.select(
|
||||
database.proxyGroups,
|
||||
)..where((t) => t.id.equals(second.id))).getSingle()).definition,
|
||||
'{"default-selected":"Primary","empty-fallback":"Primary"}',
|
||||
);
|
||||
expect(await database.proxyGroupsDao.count(profile.id).getSingle(), 2);
|
||||
|
||||
await database.proxyGroupsDao.rewrite(
|
||||
groups,
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/core/interface.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';
|
||||
@@ -17,6 +16,7 @@ import 'package:fl_clash/providers/core.dart';
|
||||
import 'package:fl_clash/providers/database.dart';
|
||||
import 'package:fl_clash/providers/state.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/ui/icons/icons.dart';
|
||||
import 'package:fl_clash/views/profiles/custom/custom_proxies.dart';
|
||||
import 'package:fl_clash/views/profiles/custom/dialers.dart';
|
||||
import 'package:fl_clash/views/profiles/custom/groups.dart';
|
||||
@@ -328,7 +328,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(menuItem(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(AppLocalizations.current.confirm));
|
||||
await tester.tap(find.text(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(notifier.value.map((rule) => rule.id), [1, 3]);
|
||||
@@ -412,7 +412,7 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(AppLocalizations.current.confirm));
|
||||
await tester.tap(find.text(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(notifier.deleted.single, {100, 102});
|
||||
@@ -497,7 +497,7 @@ void main() {
|
||||
find.text(AppLocalizations.current.confirmDeleteProxyGroup),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text(AppLocalizations.current.confirm));
|
||||
await tester.tap(find.text(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(notifier.deleted.single, {101});
|
||||
@@ -522,7 +522,39 @@ void main() {
|
||||
find.text(AppLocalizations.current.proxyGroupInUse('Group 1')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text(AppLocalizations.current.confirm));
|
||||
await tester.tap(find.text(AppLocalizations.current.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(notifier.deleted.single, {101});
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('the edit page warns when the profile still names the group', (
|
||||
tester,
|
||||
) async {
|
||||
final notifier = await _pumpProxyGroups(tester, inUse: {'Group 1'});
|
||||
|
||||
await tester.tap(find.text('Group 1'));
|
||||
await tester.pumpAndSettle();
|
||||
final delete = find.descendant(
|
||||
of: find.byType(EditProxyGroupView),
|
||||
matching: find.text(AppLocalizations.current.delete),
|
||||
);
|
||||
await tester.ensureVisible(delete);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(delete);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(AppLocalizations.current.proxyGroupInUse('Group 1')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(CommonDialog),
|
||||
matching: find.text(AppLocalizations.current.delete),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(notifier.deleted.single, {101});
|
||||
@@ -614,7 +646,11 @@ void main() {
|
||||
Finder valueIn(String title, String value) =>
|
||||
find.descendant(of: row(title), matching: find.text(value));
|
||||
Future<void> tapRow(String title) async {
|
||||
await tester.ensureVisible(row(title));
|
||||
await Scrollable.ensureVisible(
|
||||
tester.element(row(title)),
|
||||
alignment: 0.5,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(row(title));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -725,7 +761,9 @@ void main() {
|
||||
for (final title in loadBalanceOptions) {
|
||||
await add(title);
|
||||
}
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await _expectClosesWithoutIssues(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -784,7 +822,9 @@ void main() {
|
||||
await tester.enterText(fieldIn(title), '');
|
||||
}
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await _expectClosesWithoutIssues(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -843,7 +883,9 @@ void main() {
|
||||
(state) => state.copyWith(name: 'Mine', proxies: const ['DIRECT']),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await _expectClosesWithoutIssues(tester);
|
||||
|
||||
expect(proxyGroups.value.map((group) => group.name), ['Mine']);
|
||||
@@ -913,7 +955,9 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(proxyGroups.value, isEmpty);
|
||||
@@ -936,7 +980,9 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await tester.pump();
|
||||
form.update((state) => state.copyWith(name: 'Later'));
|
||||
when(() => core.validateFilters(any())).thenAnswer((_) async => ['', '']);
|
||||
|
||||
@@ -166,6 +166,23 @@ void main() {
|
||||
expect(rules.skip(1).map((rule) => rule.id), [1, 2]);
|
||||
});
|
||||
|
||||
testWidgets('drops the SUB-RULE lines', (tester) async {
|
||||
final notifier = await pumpRules(tester);
|
||||
final (:editor, :context) = await _openListEditor(tester);
|
||||
|
||||
final popped = await editor.onPop!(
|
||||
context,
|
||||
editor.title,
|
||||
'- SUB-RULE,(NETWORK,tcp),streaming\n${editor.content}',
|
||||
);
|
||||
|
||||
expect(popped, isTrue);
|
||||
expect(notifier.replaced.single.map((rule) => rule.rawValue), [
|
||||
'DOMAIN,a.com,DIRECT',
|
||||
'MATCH,DIRECT',
|
||||
]);
|
||||
});
|
||||
|
||||
testWidgets('names the line of a rule the form would refuse', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -203,7 +220,10 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: form,
|
||||
matching: find.byTooltip(currentAppLocalizations.save),
|
||||
matching: find.widgetWithText(
|
||||
TextButton,
|
||||
currentAppLocalizations.save,
|
||||
),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
@@ -14,7 +13,6 @@ import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../helpers/glyph_finders.dart';
|
||||
import '../helpers/test_app.dart';
|
||||
import '../helpers/test_profiles.dart';
|
||||
|
||||
@@ -67,6 +65,14 @@ class _TestClashProviders extends ClashProviders {
|
||||
]);
|
||||
}
|
||||
|
||||
/// The list of the open selection sheet, not the search field docked below it.
|
||||
final _selectionList = find
|
||||
.descendant(
|
||||
of: find.byWidgetPredicate((widget) => widget is SelectionSheet),
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
|
||||
class _Harness {
|
||||
late final ProviderContainer container;
|
||||
late final _RecordingProfileRules rules;
|
||||
@@ -127,7 +133,9 @@ class _Harness {
|
||||
}
|
||||
|
||||
Future<void> save(WidgetTester tester) async {
|
||||
await tester.tap(find.byGlyph(AppGlyphs.check));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, currentAppLocalizations.save),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -137,7 +145,7 @@ class _Harness {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(action.name),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
scrollable: _selectionList,
|
||||
);
|
||||
// That leaves the row at the viewport's top, under the floating bar.
|
||||
await Scrollable.ensureVisible(
|
||||
@@ -185,7 +193,7 @@ void main() {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(RuleAction.MATCH.name),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
scrollable: _selectionList,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -193,6 +201,25 @@ void main() {
|
||||
expect(find.text(RuleAction.SUB_RULE.name), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a SUB-RULE kept from before cannot be saved as one', (
|
||||
tester,
|
||||
) async {
|
||||
final harness = _Harness();
|
||||
await harness.pump(
|
||||
tester,
|
||||
initialRules: [Rule.parse('SUB-RULE,(NETWORK,tcp),streaming')],
|
||||
);
|
||||
await tester.tap(find.text(RuleAction.SUB_RULE.name));
|
||||
await tester.pumpAndSettle();
|
||||
await harness.save(tester);
|
||||
|
||||
expect(
|
||||
find.text(currentAppLocalizations.invalidSubRule('streaming')),
|
||||
findsWidgets,
|
||||
);
|
||||
expect(harness.rules.puts, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('the rule set sheet lists the app rule sets in one list', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -225,7 +252,7 @@ void main() {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(RuleAction.MATCH.name),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
scrollable: _selectionList,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(RuleAction.MATCH.name).last);
|
||||
@@ -255,7 +282,7 @@ void main() {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(RuleAction.NOT.name),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
scrollable: _selectionList,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(RuleAction.NOT.name).last);
|
||||
@@ -423,7 +450,12 @@ void main() {
|
||||
|
||||
await tester.tap(find.text(l10n.delete));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.confirm));
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(CommonDialog),
|
||||
matching: find.text(l10n.delete),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(harness.rules.deletes, [rule.id]);
|
||||
|
||||
@@ -233,7 +233,7 @@ void main() {
|
||||
}
|
||||
|
||||
Finder tile(String label) =>
|
||||
find.ancestor(of: find.text(label), matching: find.byType(CommonCard));
|
||||
find.ancestor(of: find.text(label), matching: find.byType(ListItem));
|
||||
|
||||
String? statusOf(WidgetTester tester, String label) {
|
||||
final l = currentAppLocalizations;
|
||||
@@ -251,7 +251,7 @@ void main() {
|
||||
.data;
|
||||
}
|
||||
|
||||
testWidgets('leads its proxy groups with a tile per section', (
|
||||
testWidgets('leads its proxy groups with a row per section', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpCustom(
|
||||
@@ -319,7 +319,7 @@ void main() {
|
||||
expect(find.text(l.importFile), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('keeps its tiles above an empty group list', (tester) async {
|
||||
testWidgets('keeps its rows above an empty group list', (tester) async {
|
||||
await pumpCustom(tester);
|
||||
|
||||
final l = currentAppLocalizations;
|
||||
@@ -365,6 +365,34 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TunView), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renames the profile from its title menu', (tester) async {
|
||||
await pumpCustom(tester);
|
||||
final l = currentAppLocalizations;
|
||||
Finder title(String label) =>
|
||||
find.descendant(of: find.byType(AppBar), matching: find.text(label));
|
||||
|
||||
await tester.tap(title('mine'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l.runtimeConfig), findsOneWidget);
|
||||
await tester.tap(find.text(l.rename));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(InputDialog),
|
||||
matching: find.byType(EditableText),
|
||||
),
|
||||
'theirs',
|
||||
);
|
||||
await tester.tap(find.text(l.submit));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
globalState.container.read(profilesProvider).single.label,
|
||||
'theirs',
|
||||
);
|
||||
expect(title('theirs'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('a new custom profile', () {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/features/form/rule.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/providers/app.dart';
|
||||
import 'package:fl_clash/ui/ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../helpers/test_app.dart';
|
||||
|
||||
Finder _banner(String text) =>
|
||||
find.descendant(of: find.byType(ErrorBanner), matching: find.text(text));
|
||||
|
||||
void main() {
|
||||
testWidgets('add rule validates content and returns the entered rule', (
|
||||
tester,
|
||||
@@ -24,9 +28,9 @@ void main() {
|
||||
builder: (context) {
|
||||
return FilledButton(
|
||||
onPressed: () async {
|
||||
result = await showDialog<Rule>(
|
||||
result = await showFormSheet<Rule>(
|
||||
context: context,
|
||||
builder: (_) => const AddOrEditRuleDialog(),
|
||||
builder: (_) => const RuleForm(),
|
||||
);
|
||||
},
|
||||
child: const Text('open'),
|
||||
@@ -39,15 +43,18 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
final l10n = AppLocalizations.current;
|
||||
|
||||
await tester.tap(find.text(l10n.confirm));
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.save));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.emptyTip(l10n.content)), findsOneWidget);
|
||||
expect(_banner(l10n.emptyTip(l10n.content)), findsOneWidget);
|
||||
expect(result, isNull);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'example.com');
|
||||
await tester.tap(find.text(l10n.confirm));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ErrorBanner), findsNothing);
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AddOrEditRuleDialog), findsNothing);
|
||||
expect(find.byType(RuleForm), findsNothing);
|
||||
expect(result, isNotNull);
|
||||
expect(result!.ruleAction, RuleAction.DOMAIN);
|
||||
expect(result!.content, 'example.com');
|
||||
@@ -159,9 +166,9 @@ void main() {
|
||||
builder: (context) {
|
||||
return FilledButton(
|
||||
onPressed: () async {
|
||||
result = await showDialog<Rule>(
|
||||
result = await showFormSheet<Rule>(
|
||||
context: context,
|
||||
builder: (_) => const AddOrEditRuleDialog(),
|
||||
builder: (_) => const RuleForm(),
|
||||
);
|
||||
},
|
||||
child: const Text('open'),
|
||||
@@ -175,21 +182,15 @@ void main() {
|
||||
final l10n = AppLocalizations.current;
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'example.com');
|
||||
await tester.tap(find.byType(DropdownMenu));
|
||||
await tester.tap(find.text(l10n.splitStrategy));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text(l10n.matchTarget).last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.matchTarget).last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester
|
||||
.widget<EditableText>(find.byType(EditableText).last)
|
||||
.controller
|
||||
.text,
|
||||
l10n.matchTarget,
|
||||
);
|
||||
expect(find.text(l10n.matchTarget), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(l10n.confirm));
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(result, isNotNull);
|
||||
@@ -198,7 +199,9 @@ void main() {
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('edit rule keeps the id and prefills the fields', (tester) async {
|
||||
testWidgets('edit rule keeps the id and order and prefills the fields', (
|
||||
tester,
|
||||
) async {
|
||||
Rule? result;
|
||||
const rule = Rule(
|
||||
id: 42,
|
||||
@@ -206,6 +209,7 @@ void main() {
|
||||
content: 'CN',
|
||||
ruleTarget: 'GEOIP',
|
||||
noResolve: true,
|
||||
order: 'a5',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
TestApp(
|
||||
@@ -217,9 +221,9 @@ void main() {
|
||||
builder: (context) {
|
||||
return FilledButton(
|
||||
onPressed: () async {
|
||||
result = await showDialog<Rule>(
|
||||
result = await showFormSheet<Rule>(
|
||||
context: context,
|
||||
builder: (_) => const AddOrEditRuleDialog(rule: rule),
|
||||
builder: (_) => const RuleForm(rule: rule),
|
||||
);
|
||||
},
|
||||
child: const Text('open'),
|
||||
@@ -234,15 +238,67 @@ void main() {
|
||||
expect(find.text('CN'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'US');
|
||||
await tester.tap(find.text(AppLocalizations.current.confirm));
|
||||
await tester.tap(
|
||||
find.widgetWithText(TextButton, AppLocalizations.current.save),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, 42);
|
||||
expect(result!.order, 'a5');
|
||||
expect(result!.content, 'US');
|
||||
expect(result!.ruleAction, RuleAction.GEOIP);
|
||||
expect(result!.noResolve, isTrue);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('a payload the core would refuse is held at the field', (
|
||||
tester,
|
||||
) async {
|
||||
Rule? result;
|
||||
const rule = Rule(
|
||||
id: 7,
|
||||
ruleAction: RuleAction.DST_PORT,
|
||||
content: '443',
|
||||
ruleTarget: 'DIRECT',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
TestApp(
|
||||
wrapInProviderScope: true,
|
||||
overrides: [
|
||||
viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 800)),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return FilledButton(
|
||||
onPressed: () async {
|
||||
result = await showFormSheet<Rule>(
|
||||
context: context,
|
||||
builder: (_) => const RuleForm(rule: rule),
|
||||
);
|
||||
},
|
||||
child: const Text('open'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
final l10n = AppLocalizations.current;
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'https');
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.save));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_banner(l10n.invalidRangeContent), findsOneWidget);
|
||||
expect(result, isNull);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), '80/443');
|
||||
await tester.tap(find.widgetWithText(TextButton, l10n.save));
|
||||
await tester.pumpAndSettle();
|
||||
expect(result?.content, '80/443');
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,6 +94,10 @@ void main() {
|
||||
expect(urlProfile.realAutoUpdate, true);
|
||||
expect(urlProfile.realLabel, 'Remote');
|
||||
expect(urlProfile.copyWith(label: '').realLabel, 'example.com');
|
||||
|
||||
expect(fileProfile.exportFileName, '7.yaml');
|
||||
expect(urlProfile.exportFileName, 'Remote.yaml');
|
||||
expect(urlProfile.copyWith(label: 'a.YML').exportFileName, 'a.YML');
|
||||
});
|
||||
|
||||
test('an unnamed profile falls back to its url host, then its id', () {
|
||||
|
||||
@@ -690,5 +690,31 @@ void main() {
|
||||
|
||||
expect(setup.forced, [true]);
|
||||
});
|
||||
|
||||
test(
|
||||
'a group taking in every provider or proxy reapplies only on the named',
|
||||
() async {
|
||||
await testDatabase.proxyGroups.put(
|
||||
const ProxyGroup(
|
||||
id: 23,
|
||||
name: 'All',
|
||||
type: GroupType.Selector,
|
||||
includeAllProxies: true,
|
||||
includeAllProviders: true,
|
||||
order: 'b',
|
||||
).toCompanion(profileId),
|
||||
);
|
||||
|
||||
await action().applyIfReferenced(ProviderKind.proxy, {
|
||||
'Elsewhere',
|
||||
}, force: true);
|
||||
await action().applyIfProxiesNamed({'Unnamed'});
|
||||
await action().applyIfReferenced(ProviderKind.proxy, {
|
||||
'Home',
|
||||
}, force: true);
|
||||
|
||||
expect(setup.forced, [true]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:mocktail/mocktail.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../helpers/app_paths.dart';
|
||||
import '../helpers/test_profiles.dart';
|
||||
|
||||
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
||||
@@ -513,23 +514,50 @@ proxy-groups:
|
||||
expect(config.proxyGroups.single.name, 'GLOBAL');
|
||||
});
|
||||
|
||||
test(
|
||||
'drops include-all-providers and keeps the proxies of include-all',
|
||||
() {
|
||||
final config = _read('''
|
||||
test('keeps the keys that take in every proxy or provider', () {
|
||||
final config = _read('''
|
||||
proxy-groups:
|
||||
- {name: All, type: select, include-all: true}
|
||||
- {name: Plain, type: select, proxies: [DIRECT], include-all: false}
|
||||
- {name: All, type: select, proxies: [DIRECT], include-all: true}
|
||||
- {name: Providers, type: select, include-all-providers: true}
|
||||
''');
|
||||
|
||||
final [all, providers] = config.proxyGroups;
|
||||
expect(all.includeAll, isNull);
|
||||
expect(all.includeAllProxies, isTrue);
|
||||
expect(all.includeAllProviders, isNull);
|
||||
expect(providers.includeAllProviders, isNull);
|
||||
expect(providers.includeAllProxies, isNull);
|
||||
},
|
||||
);
|
||||
final [plain, all, providers] = config.proxyGroups;
|
||||
expect(plain.definition, {
|
||||
'name': 'Plain',
|
||||
'type': 'select',
|
||||
'proxies': ['DIRECT'],
|
||||
});
|
||||
expect(all.definition, {
|
||||
'name': 'All',
|
||||
'type': 'select',
|
||||
'proxies': ['DIRECT'],
|
||||
'include-all-proxies': true,
|
||||
'include-all-providers': true,
|
||||
});
|
||||
expect(providers.definition, {
|
||||
'name': 'Providers',
|
||||
'type': 'select',
|
||||
'include-all-providers': true,
|
||||
});
|
||||
});
|
||||
|
||||
test('drops the SUB-RULE lines, since there are no sub-rules', () {
|
||||
final config = _read('''
|
||||
rules:
|
||||
- SUB-RULE,(NETWORK,tcp),streaming
|
||||
- MATCH,DIRECT
|
||||
sub-rules:
|
||||
streaming:
|
||||
- DOMAIN,video.example,DIRECT
|
||||
''');
|
||||
|
||||
expect(
|
||||
[for (final rule in config.rules) rule.rawValue],
|
||||
['MATCH,DIRECT'],
|
||||
);
|
||||
expect(config.droppedSubRules, 1);
|
||||
});
|
||||
|
||||
test('renames the proxies DNS servers dial through', () {
|
||||
final config = _read(
|
||||
@@ -657,10 +685,7 @@ tun:
|
||||
setUp(() async {
|
||||
await AppLocalizations.load(const Locale('en'));
|
||||
home = Directory.systemTemp.createTempSync('flclash-custom-import-');
|
||||
AppPath.supportDirectory = () async => home;
|
||||
AppPath.temporaryDirectory = () async => home;
|
||||
AppPath.cacheDirectory = () async => home;
|
||||
AppPath.downloadDirectory = () async => home;
|
||||
await useAppPaths(home);
|
||||
testDatabase = db.Database(NativeDatabase.memory());
|
||||
db.database = testDatabase;
|
||||
await testDatabase.profiles.put(custom.toCompanion());
|
||||
|
||||
@@ -484,15 +484,34 @@ void main() {
|
||||
expect(loop('Work', const {'Work': 'Away', 'Away': 'Home'}), isEmpty);
|
||||
});
|
||||
|
||||
test('include-all reaches a proxy only once a group declares it', () {
|
||||
test('include-all-proxies takes in only the proxies a group declares', () {
|
||||
const all = ProxyGroup(
|
||||
id: 6,
|
||||
name: 'All',
|
||||
type: GroupType.Selector,
|
||||
includeAll: true,
|
||||
includeAllProxies: true,
|
||||
);
|
||||
const exit = ProxyGroup(
|
||||
id: 7,
|
||||
name: 'Exit',
|
||||
type: GroupType.Selector,
|
||||
proxies: ['Work'],
|
||||
);
|
||||
expect(dialerLoop('All', [all], proxy: 'Home'), isEmpty);
|
||||
expect(dialerLoop('All', [all, landing], proxy: 'Home'), {'All'});
|
||||
expect(
|
||||
dialerLoop('Relay', [all, landing, relay], proxy: 'Home'),
|
||||
isEmpty,
|
||||
);
|
||||
expect(
|
||||
dialerLoop(
|
||||
'Exit',
|
||||
[exit, all, landing],
|
||||
proxy: 'Home',
|
||||
dialers: const {'Work': 'All'},
|
||||
),
|
||||
{'Exit', 'Work', 'All'},
|
||||
);
|
||||
});
|
||||
|
||||
test('what a filter picks is left to the core', () {
|
||||
@@ -500,7 +519,7 @@ void main() {
|
||||
id: 6,
|
||||
name: 'All',
|
||||
type: GroupType.Selector,
|
||||
includeAll: true,
|
||||
includeAllProxies: true,
|
||||
);
|
||||
for (final group in [
|
||||
all.copyWith(filter: '(?i)hk'),
|
||||
@@ -621,6 +640,35 @@ void main() {
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('include-all-proxies declares no app proxy of its own and drops a '
|
||||
'dialer it loops back through', () {
|
||||
const unnamed = CustomProxy(
|
||||
id: 12,
|
||||
definition: {'name': 'Unnamed', 'type': 'ss'},
|
||||
);
|
||||
const all = ProxyGroup(
|
||||
id: 6,
|
||||
name: 'All',
|
||||
type: GroupType.Selector,
|
||||
includeAllProxies: true,
|
||||
);
|
||||
expect(
|
||||
appProxiesPayload(
|
||||
[home, unnamed],
|
||||
dialers: {9: 'All', 12: 'Relay'},
|
||||
groups: const [all, landing],
|
||||
),
|
||||
[home.definition],
|
||||
);
|
||||
expect(
|
||||
const CustomProfileData(
|
||||
proxyGroups: [all, landing],
|
||||
proxies: {'Home', 'Unnamed'},
|
||||
).namedProxies,
|
||||
{'Home'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('issues', () {
|
||||
@@ -647,6 +695,28 @@ void main() {
|
||||
proxyGroupIssues(group.copyWith(includeAllProxies: true), profileData),
|
||||
isEmpty,
|
||||
);
|
||||
final providers = group.copyWith(includeAllProviders: true);
|
||||
final subscribed = profileData.copyWith(proxyProviders: const {'Sub'});
|
||||
expect(proxyGroupIssues(providers, subscribed), [
|
||||
const CustomIssue.noProxySource(),
|
||||
]);
|
||||
expect(
|
||||
proxyGroupIssues(
|
||||
providers,
|
||||
subscribed.copyWith(
|
||||
proxyGroups: [
|
||||
...subscribed.proxyGroups,
|
||||
const ProxyGroup(
|
||||
id: 4,
|
||||
name: 'D',
|
||||
type: GroupType.Selector,
|
||||
use: ['Sub'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('a group cannot take a local proxy\'s name, named here or not', () {
|
||||
@@ -683,26 +753,31 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('empty-fallback takes only a built-in', () {
|
||||
test('empty-fallback takes a built-in or a proxy a group names', () {
|
||||
const group = ProxyGroup(
|
||||
id: 3,
|
||||
name: 'C',
|
||||
type: GroupType.Selector,
|
||||
proxies: ['Node'],
|
||||
);
|
||||
for (final name in ['COMPATIBLE', 'PASS']) {
|
||||
final data = profileData.copyWith(
|
||||
proxyGroups: [
|
||||
...profileData.proxyGroups,
|
||||
group.copyWith(id: 4, name: 'D', proxies: ['Home']),
|
||||
],
|
||||
proxies: const {'Home', 'Spare'},
|
||||
);
|
||||
for (final name in ['COMPATIBLE', 'PASS', 'Home']) {
|
||||
expect(
|
||||
proxyGroupIssues(group.copyWith(emptyFallback: name), profileData),
|
||||
proxyGroupIssues(group.copyWith(emptyFallback: name), data),
|
||||
isEmpty,
|
||||
reason: name,
|
||||
);
|
||||
}
|
||||
for (final name in ['A', 'Node', 'Gone']) {
|
||||
expect(
|
||||
proxyGroupIssues(group.copyWith(emptyFallback: name), profileData),
|
||||
[CustomIssue.invalidEmptyFallback(name)],
|
||||
reason: name,
|
||||
);
|
||||
for (final name in ['A', 'Node', 'Gone', 'Spare']) {
|
||||
expect(proxyGroupIssues(group.copyWith(emptyFallback: name), data), [
|
||||
CustomIssue.invalidEmptyFallback(name),
|
||||
], reason: name);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1656,6 +1656,59 @@ void main() {
|
||||
expect((providers['plain'] as YamlMap).keys, {'type', 'path'});
|
||||
});
|
||||
|
||||
test('a custom profile group taking in every proxy or provider declares '
|
||||
'none the profile does not name', () async {
|
||||
final profile = Profile.custom(label: 'p');
|
||||
final core = _MockCoreHandlerInterface();
|
||||
final setupState = nullProfileSetupState.copyWith(
|
||||
profileId: profile.id,
|
||||
profileType: ProfileType.custom,
|
||||
proxyGroups: [
|
||||
const ProxyGroup(
|
||||
id: 1,
|
||||
name: 'g',
|
||||
type: GroupType.Selector,
|
||||
filter: 'HK',
|
||||
includeAllProxies: true,
|
||||
includeAllProviders: true,
|
||||
),
|
||||
const ProxyGroup(
|
||||
id: 2,
|
||||
name: 'h',
|
||||
type: GroupType.Selector,
|
||||
proxies: ['mine'],
|
||||
use: ['sub'],
|
||||
),
|
||||
],
|
||||
profileProviders: const {'sub': 42, 'plain': 43},
|
||||
appProxies: const [
|
||||
CustomProxy(id: 11, definition: {'name': 'mine', 'type': 'direct'}),
|
||||
CustomProxy(id: 15, definition: {'name': 'spare', 'type': 'direct'}),
|
||||
],
|
||||
);
|
||||
final scoped = ProviderContainer(
|
||||
overrides: [
|
||||
coreHandlerProvider.overrideWithValue(CoreController.scoped(core)),
|
||||
setupActionProvider.overrideWith(SetupAction.new),
|
||||
],
|
||||
);
|
||||
addTearDown(scoped.dispose);
|
||||
|
||||
final res = await scoped
|
||||
.read(setupActionProvider.notifier)
|
||||
.getProfile(
|
||||
setupState: setupState,
|
||||
patchConfig: const PatchClashConfig(),
|
||||
);
|
||||
final config = loadYaml(res.yaml) as YamlMap;
|
||||
|
||||
expect((config['proxy-providers'] as YamlMap).keys, {'sub'});
|
||||
expect([for (final proxy in config['proxies']) proxy['name']], ['mine']);
|
||||
final group = (config['proxy-groups'] as YamlList).first as YamlMap;
|
||||
expect(group['include-all-proxies'], isTrue);
|
||||
expect(group['include-all-providers'], isTrue);
|
||||
});
|
||||
|
||||
test('only a subscription profile runs its script extension', () async {
|
||||
final engine = scriptEvaluator;
|
||||
addTearDown(() => scriptEvaluator = engine);
|
||||
|
||||
@@ -444,6 +444,13 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(offered('REJECT'), findsNothing);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: picker,
|
||||
matching: find.textContaining('${l.filters} ('),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
for (final header in [
|
||||
'${l.basicStrategy} (5)',
|
||||
'${l.localProxies} (1)',
|
||||
@@ -452,13 +459,6 @@ void main() {
|
||||
await tester.tap(offered(header));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: picker,
|
||||
matching: find.textContaining('${l.filters} ('),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(offered('DIRECT'), findsNothing);
|
||||
expect(offered('mine'), findsNothing);
|
||||
expect(offered(l.url), findsOneWidget);
|
||||
@@ -472,6 +472,11 @@ void main() {
|
||||
expect(container.read(proxyGroupProvider).proxies, ['DIRECT']);
|
||||
await tester.tap(offered('REJECT'));
|
||||
await tester.tap(offered('Home'));
|
||||
await Scrollable.ensureVisible(
|
||||
tester.element(offered('p')),
|
||||
alignment: 0.5,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(offered('p'));
|
||||
await tester.scrollUntilVisible(
|
||||
offered(_hongKong.label),
|
||||
@@ -499,6 +504,82 @@ void main() {
|
||||
expect(group.filter, 'mine`${_hongKong.regex}');
|
||||
});
|
||||
|
||||
testWidgets('the picker adds taking in every proxy or provider as a member '
|
||||
'beside the named ones', (tester) async {
|
||||
final l = currentAppLocalizations;
|
||||
container = buildContainer(
|
||||
_group(proxies: ['DIRECT']),
|
||||
customProxies: const [_home],
|
||||
);
|
||||
final picker = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget.runtimeType.toString() == '_MemberPicker<ProxyGroup>',
|
||||
);
|
||||
Finder offered(String name) =>
|
||||
find.descendant(of: picker, matching: find.text(name));
|
||||
Finder listed(String name) => find.descendant(
|
||||
of: find.byType(SuperSliverReorderableList),
|
||||
matching: find.text(name),
|
||||
);
|
||||
|
||||
await pumpGroupSheet(tester);
|
||||
await tester.ensureVisible(find.text(l.addNodes));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l.addNodes));
|
||||
await tester.pumpAndSettle();
|
||||
for (final header in [
|
||||
'${l.autoInclude} (2)',
|
||||
'${l.proxyProviders} (1)',
|
||||
]) {
|
||||
await tester.tap(offered(header));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
await tester.tap(offered('p'));
|
||||
await tester.tap(offered(l.allReferencedProxyProviders));
|
||||
await tester.tap(offered(l.allReferencedProxies));
|
||||
await tester.pump();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final group = container.read(proxyGroupProvider);
|
||||
expect(group.proxies, ['DIRECT']);
|
||||
expect(group.use, ['p']);
|
||||
expect(group.includeAllProxies, isTrue);
|
||||
expect(group.includeAllProviders, isTrue);
|
||||
expect(listed(l.allReferencedProxies), findsOneWidget);
|
||||
expect(listed(l.allReferencedProxyProviders), findsOneWidget);
|
||||
expect(listed('p'), findsOneWidget);
|
||||
|
||||
await tester.ensureVisible(find.text(l.addNodes));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l.addNodes));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(offered('${l.localProxies} (1)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(offered('Home'), findsOneWidget);
|
||||
for (final section in [l.autoInclude, l.proxyProviders]) {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: picker,
|
||||
matching: find.textContaining('$section ('),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
}
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: rowOf(l.allReferencedProxyProviders),
|
||||
matching: find.byTooltip(l.remove),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(container.read(proxyGroupProvider).includeAllProviders, isNull);
|
||||
expect(container.read(proxyGroupProvider).includeAllProxies, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('the picker adds exclude filters as members of their own', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -11,28 +11,13 @@ import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
|
||||
import '../helpers/app_paths.dart';
|
||||
import '../helpers/test_app.dart';
|
||||
import '../helpers/test_profiles.dart';
|
||||
|
||||
const _profileId = 1;
|
||||
|
||||
class _FakePathProvider extends PathProviderPlatform {
|
||||
final String root;
|
||||
|
||||
_FakePathProvider(this.root);
|
||||
|
||||
@override
|
||||
Future<String?> getTemporaryPath() async => root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async => root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationCachePath() async => root;
|
||||
}
|
||||
|
||||
class _TestProxyGroups extends ProxyGroups {
|
||||
_TestProxyGroups(this.groups);
|
||||
|
||||
@@ -73,10 +58,10 @@ void main() {
|
||||
|
||||
late ProviderContainer container;
|
||||
|
||||
setUpAll(() {
|
||||
PathProviderPlatform.instance = _FakePathProvider(
|
||||
Directory.systemTemp.createTempSync('custom_providers').path,
|
||||
);
|
||||
setUpAll(() async {
|
||||
final home = Directory.systemTemp.createTempSync('custom_providers');
|
||||
addTearDown(() => home.deleteSync(recursive: true));
|
||||
await useAppPaths(home);
|
||||
});
|
||||
|
||||
Future<void> pumpView(
|
||||
@@ -209,7 +194,7 @@ void main() {
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextFormField), '600');
|
||||
await tester.tap(find.byTooltip(l.save));
|
||||
await tester.tap(find.widgetWithText(TextButton, l.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -241,7 +226,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(l.save));
|
||||
await tester.tap(find.widgetWithText(TextButton, l.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -272,7 +257,7 @@ void main() {
|
||||
}
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(l.save));
|
||||
await tester.tap(find.widgetWithText(TextButton, l.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -327,7 +312,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(fieldIn(l.testUrl), '');
|
||||
await tester.enterText(fieldIn(l.testInterval), '');
|
||||
await tester.tap(find.byTooltip(l.save));
|
||||
await tester.tap(find.widgetWithText(TextButton, l.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -386,7 +371,7 @@ void main() {
|
||||
find.descendant(of: excluded, matching: find.byTooltip(l.remove)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip(l.save));
|
||||
await tester.tap(find.widgetWithText(TextButton, l.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
|
||||
@@ -431,9 +431,7 @@ void main() {
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('shows the loading illustration until every row is ready', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('shows every row in one step once all are ready', (tester) async {
|
||||
const size = Size(1000, 1400);
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -469,21 +467,13 @@ void main() {
|
||||
);
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pump();
|
||||
await settle(tester);
|
||||
|
||||
final l10n = currentAppLocalizations;
|
||||
final illustration = find.byKey(
|
||||
const ValueKey(NullStatusIllustration.data),
|
||||
);
|
||||
expect(illustration, findsOneWidget);
|
||||
expect(find.text(GeoResource.MMDB.name), findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
final l10n = currentAppLocalizations;
|
||||
await pumpUntilFound(tester, find.text(GeoResource.MMDB.name));
|
||||
expect(find.text('Remote script'), findsOneWidget);
|
||||
expect(find.text(l10n.unknown), findsNWidgets(GeoResource.values.length));
|
||||
await tester.pumpAndSettle();
|
||||
expect(illustration, findsNothing);
|
||||
expect(tester.takeException(), null);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/theme.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
@@ -12,23 +11,11 @@ import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
class _FakePathProvider extends PathProviderPlatform {
|
||||
final String root;
|
||||
|
||||
_FakePathProvider(this.root);
|
||||
|
||||
@override
|
||||
Future<String?> getTemporaryPath() async => root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async => root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationCachePath() async => root;
|
||||
}
|
||||
import '../helpers/app_paths.dart';
|
||||
import '../helpers/test_app.dart';
|
||||
import '../helpers/test_profiles.dart';
|
||||
|
||||
/// The edit page disposes the profile by scheduling an auto-apply; keep that a
|
||||
/// no-op so the setup machinery never runs in a widget test.
|
||||
@@ -37,13 +24,6 @@ class _NoopSetupAction extends SetupAction {
|
||||
void autoApplyProfile() {}
|
||||
}
|
||||
|
||||
class _RecordingProfilesAction extends ProfilesAction {
|
||||
final put = <Profile>[];
|
||||
|
||||
@override
|
||||
void putProfile(Profile profile) => put.add(profile);
|
||||
}
|
||||
|
||||
Profile _urlProfile() =>
|
||||
Profile.normal(label: 'test', url: 'https://example.com/sub');
|
||||
|
||||
@@ -85,13 +65,12 @@ Future<FocusNode> pumpEditProfile(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
AppUiLocalizations.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
builder: (context, child) {
|
||||
globalState.measure = Measure.of(context, 1);
|
||||
globalState.theme = CommonTheme.of(context, 1);
|
||||
return child!;
|
||||
return wrapAppScopes(context, child!);
|
||||
},
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
@@ -104,7 +83,6 @@ Future<FocusNode> pumpEditProfile(
|
||||
child: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: EditProfileView(
|
||||
context: context,
|
||||
profile: profile ?? _urlProfile(),
|
||||
),
|
||||
),
|
||||
@@ -143,9 +121,7 @@ void main() {
|
||||
|
||||
setUpAll(() async {
|
||||
tempDir = Directory.systemTemp.createTempSync('edit_profile_focus_test');
|
||||
PathProviderPlatform.instance = _FakePathProvider(tempDir.path);
|
||||
// A path resolved inside one test's fake zone never wakes a later test.
|
||||
await appPath.dataDir.future;
|
||||
await useAppPaths(tempDir);
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
@@ -172,28 +148,6 @@ void main() {
|
||||
expect(find.byTooltip('Save'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a custom profile edits only its name', (tester) async {
|
||||
final action = _RecordingProfilesAction();
|
||||
final profile = Profile.custom(label: 'Mine');
|
||||
await pumpEditProfile(
|
||||
tester,
|
||||
profile: profile,
|
||||
overrides: [profilesActionProvider.overrideWith(() => action)],
|
||||
);
|
||||
|
||||
expect(find.byType(TextFormField), findsOneWidget);
|
||||
expect(find.text(currentAppLocalizations.autoUpdate), findsNothing);
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, currentAppLocalizations.name),
|
||||
'Renamed',
|
||||
);
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.save));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(action.put.single, profile.copyWith(label: 'Renamed'));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the profile file row shows its size and update time without chips',
|
||||
(tester) async {
|
||||
@@ -220,4 +174,33 @@ void main() {
|
||||
expect(find.byType(MetaChip), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('saving with auto update off ignores an emptied interval', (
|
||||
tester,
|
||||
) async {
|
||||
final profile = _urlProfile();
|
||||
await pumpEditProfile(
|
||||
tester,
|
||||
profile: profile,
|
||||
overrides: [
|
||||
profilesProvider.overrideWith(() => TestProfiles([profile])),
|
||||
],
|
||||
);
|
||||
final interval = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TextField &&
|
||||
widget.controller?.text == '${profile.autoUpdateDuration.inMinutes}',
|
||||
);
|
||||
|
||||
await tester.enterText(interval, '');
|
||||
await tester.tap(find.byType(Switch));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Save'));
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
final saved = globalState.container.read(profilesProvider).single;
|
||||
expect(saved.autoUpdate, isFalse);
|
||||
expect(saved.autoUpdateDuration, profile.autoUpdateDuration);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/theme.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
@@ -14,6 +13,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../helpers/context_menu.dart';
|
||||
import '../helpers/test_app.dart';
|
||||
import '../helpers/test_profiles.dart';
|
||||
|
||||
Profile urlProfile(String label) =>
|
||||
@@ -44,6 +44,7 @@ Future<ProviderContainer> pumpProfiles(
|
||||
required List<Profile> profiles,
|
||||
int? currentProfileId,
|
||||
ValueListenable<bool>? isActive,
|
||||
bool picker = false,
|
||||
List<Override> overrides = const [],
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(900, 800);
|
||||
@@ -71,21 +72,20 @@ Future<ProviderContainer> pumpProfiles(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
AppUiLocalizations.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
builder: (context, child) {
|
||||
globalState.measure = Measure.of(context, 1);
|
||||
globalState.theme = CommonTheme.of(context, 1);
|
||||
return child!;
|
||||
return wrapAppScopes(context, child!);
|
||||
},
|
||||
home: isActive == null
|
||||
? const ProfilesView()
|
||||
? ProfilesView(picker: picker)
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: isActive,
|
||||
builder: (_, isActive, child) =>
|
||||
PageActivityScope(isActive: isActive, child: child!),
|
||||
child: const ProfilesView(),
|
||||
child: ProfilesView(picker: picker),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -95,8 +95,7 @@ Future<ProviderContainer> pumpProfiles(
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('a focused profile card opens its menu and reaches its '
|
||||
'subscription info', (tester) async {
|
||||
testWidgets('a focused profile card opens its menu', (tester) async {
|
||||
final profiles = [
|
||||
urlProfile('url 1'),
|
||||
Profile.normal(label: 'file 1'),
|
||||
@@ -105,7 +104,7 @@ void main() {
|
||||
await pumpProfiles(tester, profiles: profiles);
|
||||
|
||||
final cards = find
|
||||
.byType(OutlinedButton)
|
||||
.byType(FilledButton)
|
||||
.evaluate()
|
||||
.map((element) => element.widget)
|
||||
.toList();
|
||||
@@ -114,10 +113,8 @@ void main() {
|
||||
for (var tab = 0; tab < 50; tab++) {
|
||||
final context = FocusManager.instance.primaryFocus?.context;
|
||||
final focusedCard = context
|
||||
?.findAncestorWidgetOfExactType<OutlinedButton>();
|
||||
final focusedAction = context
|
||||
?.findAncestorWidgetOfExactType<IconButton>();
|
||||
if (focusedAction == null && identical(focusedCard, cards[index])) {
|
||||
?.findAncestorWidgetOfExactType<FilledButton>();
|
||||
if (identical(focusedCard, cards[index])) {
|
||||
return;
|
||||
}
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
@@ -142,23 +139,6 @@ void main() {
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CommonPopupMenu), findsNothing);
|
||||
|
||||
if (profiles[profileIndex].subscriptionInfo == null) {
|
||||
continue;
|
||||
}
|
||||
await focusCard(profileIndex);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pump();
|
||||
|
||||
final context = FocusManager.instance.primaryFocus?.context;
|
||||
expect(
|
||||
context?.findAncestorWidgetOfExactType<IconButton>()?.tooltip,
|
||||
currentAppLocalizations.subscriptionInfo,
|
||||
);
|
||||
expect(
|
||||
context?.findAncestorWidgetOfExactType<ListItem>()?.key,
|
||||
Key(profiles[profileIndex].id.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,15 +171,60 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the usage icon opens the subscription dialog', (tester) async {
|
||||
await pumpProfiles(tester, profiles: [urlProfile('url')]);
|
||||
testWidgets('a card without traffic names its source under the title and '
|
||||
'has no subscription info button', (tester) async {
|
||||
final l = currentAppLocalizations;
|
||||
await pumpProfiles(
|
||||
tester,
|
||||
profiles: [
|
||||
Profile.normal(label: 'url', url: 'https://example.com/sub'),
|
||||
Profile.normal(label: 'file'),
|
||||
Profile.custom(label: 'mine'),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip(currentAppLocalizations.subscriptionInfo));
|
||||
await tester.pumpAndSettle();
|
||||
for (final (label, source) in [
|
||||
('url', l.remote),
|
||||
('file', l.local),
|
||||
('mine', l.custom),
|
||||
]) {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.ancestor(
|
||||
of: find.text(label),
|
||||
matching: find.byType(ProfileItem),
|
||||
),
|
||||
matching: find.text(source),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
find.byTooltip(currentAppLocalizations.subscriptionInfo),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
expect(find.byType(CommonDialog), findsOneWidget);
|
||||
expect(find.byType(SubscriptionInfoDetailView), findsOneWidget);
|
||||
expect(find.text(currentAppLocalizations.subscriptionInfo), findsOneWidget);
|
||||
testWidgets('a custom card counts its groups in place of an update time', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpProfiles(
|
||||
tester,
|
||||
profiles: [Profile.custom(label: 'mine')],
|
||||
overrides: [
|
||||
profileGroupsCountProvider.overrideWith((_, _) => Stream.value(6)),
|
||||
],
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
Finder inCard(Finder matching) =>
|
||||
find.descendant(of: find.byType(ProfileItem), matching: matching);
|
||||
expect(inCard(find.widgetWithText(TonalChip, '6')), findsOneWidget);
|
||||
expect(
|
||||
inCard(find.byTooltip(currentAppLocalizations.proxyGroupsCount(6, '6'))),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(inCard(find.byType(LastUpdateTimeText)), findsNothing);
|
||||
});
|
||||
|
||||
Future<void> openProfileMenu(WidgetTester tester, String label) async {
|
||||
@@ -214,12 +239,12 @@ void main() {
|
||||
|
||||
await openProfileMenu(tester, 'url');
|
||||
expect(find.text(l.extend), findsOneWidget);
|
||||
expect(find.text(l.finalConfig), findsNothing);
|
||||
expect(find.text(l.runtimeConfig), findsNothing);
|
||||
expect(find.text(l.rename), findsNothing);
|
||||
|
||||
await tester.tap(find.text(l.more).last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l.finalConfig), findsOneWidget);
|
||||
expect(find.text(l.runtimeConfig), findsOneWidget);
|
||||
expect(find.text(l.subscriptionInfo), findsNothing);
|
||||
});
|
||||
|
||||
@@ -230,7 +255,7 @@ void main() {
|
||||
await pumpProfiles(tester, profiles: [Profile.custom(label: 'mine')]);
|
||||
|
||||
await openProfileMenu(tester, 'mine');
|
||||
expect(find.text(l.finalConfig), findsOneWidget);
|
||||
expect(find.text(l.runtimeConfig), findsOneWidget);
|
||||
expect(find.text(l.extend), findsNothing);
|
||||
expect(find.text(l.more), findsNothing);
|
||||
});
|
||||
@@ -266,7 +291,7 @@ void main() {
|
||||
|
||||
expect(find.textContaining('Home is still used'), findsOneWidget);
|
||||
expect(find.textContaining('used by Work'), findsOneWidget);
|
||||
await tester.tap(find.text(currentAppLocalizations.confirm));
|
||||
await tester.tap(find.text(currentAppLocalizations.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(action.deleted, [home.id]);
|
||||
@@ -303,6 +328,20 @@ void main() {
|
||||
expectRevealed(tester);
|
||||
});
|
||||
|
||||
testWidgets('not in a picker, whose bar is left empty', (tester) async {
|
||||
await pumpProfiles(
|
||||
tester,
|
||||
profiles: profiles,
|
||||
currentProfileId: selected.id,
|
||||
picker: true,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(profiles.first.label), findsOneWidget);
|
||||
expect(find.text(selected.label), findsNothing);
|
||||
expect(find.byType(AppBarActionButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('not when the kept page becomes active again', (tester) async {
|
||||
final isActive = ValueNotifier(true);
|
||||
addTearDown(isActive.dispose);
|
||||
|
||||
Reference in New Issue
Block a user