From bee2f8aa4f013809b488c0b2668c5b12c2fcca7d Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 4 Aug 2024 08:21:14 +0800 Subject: [PATCH] Optimize provider page Optimize delay test Support local backup and recovery --- core/Clash.Meta | 2 +- core/common.go | 169 +++++++--- core/hub.go | 176 ++++++---- core/state.go | 5 +- lib/clash/core.dart | 47 +-- lib/clash/generated/clash_ffi.dart | 39 +-- lib/common/archive.dart | 28 ++ lib/common/color.dart | 8 + lib/common/constant.dart | 2 +- lib/common/dav_client.dart | 62 +--- lib/common/navigation.dart | 2 +- lib/common/other.dart | 5 + lib/common/path.dart | 9 + lib/common/picker.dart | 16 +- lib/common/window.dart | 1 + lib/controller.dart | 99 ++++-- lib/fragments/access.dart | 105 +++--- lib/fragments/backup_and_recovery.dart | 372 ++++++++++++--------- lib/fragments/config.dart | 105 +++++- lib/fragments/profiles/edit_profile.dart | 30 +- lib/fragments/profiles/profiles.dart | 315 ++++++++--------- lib/fragments/profiles/view_profile.dart | 232 +++++++++++++ lib/fragments/proxies/common.dart | 10 +- lib/fragments/proxies/providers.dart | 187 +++++++++++ lib/fragments/proxies/proxies.dart | 46 ++- lib/fragments/resources.dart | 218 +----------- lib/l10n/arb/intl_en.arb | 15 +- lib/l10n/arb/intl_zh_CN.arb | 15 +- lib/l10n/intl/messages_en.dart | 22 +- lib/l10n/intl/messages_zh_CN.dart | 15 +- lib/l10n/l10n.dart | 114 +++++-- lib/models/app.dart | 30 ++ lib/models/clash_config.dart | 12 + lib/models/config.dart | 1 + lib/models/ffi.dart | 11 +- lib/models/generated/clash_config.g.dart | 2 + lib/models/generated/config.freezed.dart | 27 +- lib/models/generated/config.g.dart | 2 + lib/models/generated/ffi.freezed.dart | 116 +++++-- lib/models/generated/ffi.g.dart | 10 +- lib/models/generated/profile.freezed.dart | 42 ++- lib/models/generated/selector.freezed.dart | 148 ++++---- lib/models/profile.dart | 9 +- lib/models/proxy.dart | 2 +- lib/models/selector.dart | 12 +- lib/state.dart | 12 +- lib/widgets/builder.dart | 29 +- lib/widgets/card.dart | 61 ++-- lib/widgets/clash_container.dart | 42 ++- lib/widgets/sheet.dart | 25 +- lib/widgets/window_container.dart | 18 +- pubspec.lock | 34 +- pubspec.yaml | 5 +- 53 files changed, 1993 insertions(+), 1128 deletions(-) create mode 100644 lib/common/archive.dart create mode 100644 lib/fragments/profiles/view_profile.dart create mode 100644 lib/fragments/proxies/providers.dart diff --git a/core/Clash.Meta b/core/Clash.Meta index 3d773d7..fffdf84 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 3d773d7fa599f30ad28e1ad22e8bf91a2a446f7d +Subproject commit fffdf84493f054423b23e6883bcc2cdcfe877439 diff --git a/core/common.go b/core/common.go index cb4e9ee..45056e0 100644 --- a/core/common.go +++ b/core/common.go @@ -2,19 +2,23 @@ package main import "C" import ( + "context" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" - ap "github.com/metacubex/mihomo/adapter/provider" + "github.com/metacubex/mihomo/adapter/provider" + "github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" + cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/log" + rp "github.com/metacubex/mihomo/rules/provider" "github.com/metacubex/mihomo/tunnel" "os" "os/exec" @@ -26,40 +30,40 @@ import ( "time" ) -type healthCheckSchema struct { - Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` - TestTimeout int `provider:"timeout,omitempty"` - Lazy bool `provider:"lazy,omitempty"` - ExpectedStatus string `provider:"expected-status,omitempty"` -} +//type healthCheckSchema struct { +// Enable bool `provider:"enable"` +// URL string `provider:"url"` +// Interval int `provider:"interval"` +// TestTimeout int `provider:"timeout,omitempty"` +// Lazy bool `provider:"lazy,omitempty"` +// ExpectedStatus string `provider:"expected-status,omitempty"` +//} -type proxyProviderSchema struct { - Type string `provider:"type"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Interval int `provider:"interval,omitempty"` - Filter string `provider:"filter,omitempty"` - ExcludeFilter string `provider:"exclude-filter,omitempty"` - ExcludeType string `provider:"exclude-type,omitempty"` - DialerProxy string `provider:"dialer-proxy,omitempty"` - - HealthCheck healthCheckSchema `provider:"health-check,omitempty"` - Override ap.OverrideSchema `provider:"override,omitempty"` - Header map[string][]string `provider:"header,omitempty"` -} - -type ruleProviderSchema struct { - Type string `provider:"type"` - Behavior string `provider:"behavior"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Format string `provider:"format,omitempty"` - Interval int `provider:"interval,omitempty"` -} +//type proxyProviderSchema struct { +// Type string `provider:"type"` +// Path string `provider:"path,omitempty"` +// URL string `provider:"url,omitempty"` +// Proxy string `provider:"proxy,omitempty"` +// Interval int `provider:"interval,omitempty"` +// Filter string `provider:"filter,omitempty"` +// ExcludeFilter string `provider:"exclude-filter,omitempty"` +// ExcludeType string `provider:"exclude-type,omitempty"` +// DialerProxy string `provider:"dialer-proxy,omitempty"` +// +// HealthCheck healthCheckSchema `provider:"health-check,omitempty"` +// Override ap.OverrideSchema `provider:"override,omitempty"` +// Header map[string][]string `provider:"header,omitempty"` +//} +// +//type ruleProviderSchema struct { +// Type string `provider:"type"` +// Behavior string `provider:"behavior"` +// Path string `provider:"path,omitempty"` +// URL string `provider:"url,omitempty"` +// Proxy string `provider:"proxy,omitempty"` +// Format string `provider:"format,omitempty"` +// Interval int `provider:"interval,omitempty"` +//} type ConfigExtendedParams struct { IsPatch bool `json:"is-patch"` @@ -69,9 +73,9 @@ type ConfigExtendedParams struct { } type GenerateConfigParams struct { - ProfilePath *string `json:"profile-path"` - Config config.RawConfig `json:"config" ` - Params ConfigExtendedParams `json:"params"` + ProfileId string `json:"profile-id"` + Config config.RawConfig `json:"config" ` + Params ConfigExtendedParams `json:"params"` } type ChangeProxyParams struct { @@ -93,9 +97,13 @@ type ExternalProvider struct { Name string `json:"name"` Type string `json:"type"` VehicleType string `json:"vehicle-type"` + Count int `json:"count"` + Path string `json:"path"` UpdateAt time.Time `json:"update-at"` } +var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) + func restartExecutable(execPath string) { var err error executor.Shutdown() @@ -145,26 +153,76 @@ func removeFile(path string) error { return nil } -func getRawConfigWithPath(path *string) *config.RawConfig { - if path == nil { - return config.DefaultRawConfig() - } else { - bytes, err := readFile(*path) - if err != nil { - log.Errorln("getProfile readFile error %v", err) - return config.DefaultRawConfig() - } - prof, err := config.UnmarshalRawConfig(bytes) - if err != nil { - log.Errorln("getProfile UnmarshalRawConfig error %v", err) - return config.DefaultRawConfig() - } - return prof - } +func getProfilePath(id string) string { + return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml") } -func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig { - prof := getRawConfigWithPath(profilePath) +func getProfileProvidersPath(id string) string { + return filepath.Join(constant.Path.HomeDir(), "providers", id) +} + +func getRawConfigWithId(id string) *config.RawConfig { + path := getProfilePath(id) + bytes, err := readFile(path) + if err != nil { + log.Errorln("profile is not exist") + return config.DefaultRawConfig() + } + prof, err := config.UnmarshalRawConfig(bytes) + if err != nil { + log.Errorln("unmarshalRawConfig error %v", err) + return config.DefaultRawConfig() + } + for _, mapping := range prof.ProxyProvider { + value, exist := mapping["path"].(string) + if !exist { + continue + } + mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) + } + for _, mapping := range prof.RuleProvider { + value, exist := mapping["path"].(string) + if !exist { + continue + } + mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) + } + return prof +} + +func getExternalProvidersRaw() map[string]ExternalProvider { + externalProviders := make(map[string]ExternalProvider) + for n, p := range tunnel.Providers() { + if p.VehicleType() != cp.Compatible { + p := p.(*provider.ProxySetProvider) + externalProviders[n] = ExternalProvider{ + Name: n, + Type: p.Type().String(), + VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), + UpdateAt: p.UpdatedAt, + } + } + } + for n, p := range tunnel.RuleProviders() { + if p.VehicleType() != cp.Compatible { + p := p.(*rp.RuleSetProvider) + externalProviders[n] = ExternalProvider{ + Name: n, + Type: p.Type().String(), + VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), + UpdateAt: p.UpdatedAt, + } + } + } + return externalProviders +} + +func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig { + prof := getRawConfigWithId(profileId) overwriteConfig(prof, cfg) return prof } @@ -327,6 +385,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi targetConfig.LogLevel = patchConfig.LogLevel targetConfig.Port = 0 targetConfig.SocksPort = 0 + targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval targetConfig.MixedPort = patchConfig.MixedPort targetConfig.FindProcessMode = patchConfig.FindProcessMode targetConfig.AllowLan = patchConfig.AllowLan diff --git a/core/hub.go b/core/hub.go index e6d6c6b..355b0d7 100644 --- a/core/hub.go +++ b/core/hub.go @@ -11,7 +11,6 @@ import ( "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/provider" - "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" @@ -35,8 +34,6 @@ var configParams = ConfigExtendedParams{} var isInit = false -var currentProfileName = "" - //export initClash func initClash(homeDirStr *C.char) bool { if !isInit { @@ -75,16 +72,6 @@ func forceGc() { }() } -//export setCurrentProfileName -func setCurrentProfileName(s *C.char) { - currentProfileName = C.GoString(s) -} - -//export getCurrentProfileName -func getCurrentProfileName() *C.char { - return C.CString(currentProfileName) -} - //export validateConfig func validateConfig(s *C.char, port C.longlong) { i := int64(port) @@ -111,7 +98,7 @@ func updateConfig(s *C.char, port C.longlong) { return } configParams = params.Params - prof := decorationConfig(params.ProfilePath, params.Config) + prof := decorationConfig(params.ProfileId, params.Config) currentConfig = prof err = applyConfig() if err != nil { @@ -124,34 +111,10 @@ func updateConfig(s *C.char, port C.longlong) { //export clearEffect func clearEffect(s *C.char) { - path := C.GoString(s) + id := C.GoString(s) go func() { - rawCfg := getRawConfigWithPath(&path) - for _, mapping := range rawCfg.RuleProvider { - schema := &ruleProviderSchema{} - decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - if err := decoder.Decode(mapping, schema); err != nil { - return - } - if schema.Type == "http" { - _ = removeFile(constant.Path.Resolve(schema.Path)) - } - } - for _, mapping := range rawCfg.ProxyProvider { - schema := &proxyProviderSchema{ - HealthCheck: healthCheckSchema{ - Lazy: true, - }, - } - decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - if err := decoder.Decode(mapping, schema); err != nil { - return - } - if schema.Type == "http" { - _ = removeFile(constant.Path.Resolve(schema.Path)) - } - } - _ = removeFile(path) + _ = removeFile(getProfilePath(id)) + _ = removeFile(getProfileProvidersPath(id)) }() } @@ -184,10 +147,13 @@ func changeProxy(s *C.char) { if !ok { return } - - err = selector.Set(proxyName) + if proxyName == "" { + selector.ForceSet(proxyName) + } else { + err = selector.Set(proxyName) + } if err == nil { - log.Infoln("[Selector] %s selected %s", groupName, proxyName) + log.Infoln("[SelectAble] %s selected %s", groupName, proxyName) } } @@ -230,16 +196,16 @@ func resetTraffic() { func asyncTestDelay(s *C.char, port C.longlong) { i := int64(port) paramsString := C.GoString(s) - go func() { + b.Go(paramsString, func() (bool, error) { var params = &TestDelayParams{} err := json.Unmarshal([]byte(paramsString), params) if err != nil { - return + return false, nil } expectedStatus, err := utils.NewUnsignedRanges[uint16]("") if err != nil { - return + return false, nil } ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout)) @@ -256,7 +222,7 @@ func asyncTestDelay(s *C.char, port C.longlong) { delayData.Value = -1 data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return + return false, nil } delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) @@ -264,14 +230,14 @@ func asyncTestDelay(s *C.char, port C.longlong) { delayData.Value = -1 data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return + return false, nil } delayData.Value = int32(delay) data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return - }() + return false, nil + }) } //export getVersionInfo @@ -345,28 +311,31 @@ func getProvider(name *C.char) *C.char { //export getExternalProviders func getExternalProviders() *C.char { - externalProviders := make([]ExternalProvider, 0) - providers := tunnel.Providers() - for n, p := range providers { + externalProviders := make(map[string]ExternalProvider) + for n, p := range tunnel.Providers() { if p.VehicleType() != cp.Compatible { p := p.(*provider.ProxySetProvider) - externalProviders = append(externalProviders, ExternalProvider{ + externalProviders[n] = ExternalProvider{ Name: n, Type: p.Type().String(), VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), UpdateAt: p.UpdatedAt, - }) + } } } for n, p := range tunnel.RuleProviders() { if p.VehicleType() != cp.Compatible { p := p.(*rp.RuleSetProvider) - externalProviders = append(externalProviders, ExternalProvider{ + externalProviders[n] = ExternalProvider{ Name: n, Type: p.Type().String(), VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), UpdateAt: p.UpdatedAt, - }) + } } } data, err := json.Marshal(externalProviders) @@ -376,6 +345,21 @@ func getExternalProviders() *C.char { return C.CString(string(data)) } +//export getExternalProvider +func getExternalProvider(name *C.char) *C.char { + externalProviderName := C.GoString(name) + externalProviders := getExternalProvidersRaw() + externalProvider, exist := externalProviders[externalProviderName] + if !exist { + return C.CString("") + } + data, err := json.Marshal(externalProvider) + if err != nil { + return C.CString("") + } + return C.CString(string(data)) +} + //export updateExternalProvider func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) { i := int64(port) @@ -385,14 +369,24 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l switch providerTypeString { case "Proxy": providers := tunnel.Providers() - err := providers[providerNameString].Update() + proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) + if !exist { + bridge.SendToPort(i, "proxy provider is not exist") + return + } + err := proxyProvider.Update() if err != nil { bridge.SendToPort(i, err.Error()) return } case "Rule": providers := tunnel.RuleProviders() - err := providers[providerNameString].Update() + ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) + if !exist { + bridge.SendToPort(i, "rule provider is not exist") + return + } + err := ruleProvider.Update() if err != nil { bridge.SendToPort(i, err.Error()) return @@ -426,6 +420,66 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l }() } +//func sideLoadExternalProvider(providerName *C.char, providerType *C.char, data *C.char, port C.longlong) { +// i := int64(port) +// bytes := []byte(C.GoString(data)) +// providerNameString := C.GoString(providerName) +// providerTypeString := C.GoString(providerType) +// go func() { +// switch providerTypeString { +// case "Proxy": +// providers := tunnel.Providers() +// proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) +// if exist { +// bridge.SendToPort(i, "proxy provider is not exist") +// return +// } +// err := proxyProvider.Update() +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "Rule": +// providers := tunnel.RuleProviders() +// ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) +// if exist { +// bridge.SendToPort(i, "proxy provider is not exist") +// return +// } +// err := ruleProvider.Update() +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "MMDB": +// err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "ASN": +// err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "GeoIp": +// err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "GeoSite": +// err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// } +// bridge.SendToPort(i, "") +// }() +//} + //export initNativeApiBridge func initNativeApiBridge(api unsafe.Pointer) { bridge.InitDartApi(api) @@ -463,7 +517,7 @@ func init() { Data: c, }) } - executor.DefaultProxyProviderLoadedHook = func(providerName string) { + executor.DefaultProviderLoadedHook = func(providerName string) { SendMessage(Message{ Type: LoadedMessage, Data: providerName, diff --git a/core/state.go b/core/state.go index c71a4ab..3034a92 100644 --- a/core/state.go +++ b/core/state.go @@ -21,8 +21,9 @@ type AndroidProps struct { type State struct { AndroidProps - MixedPort int `json:"mixedPort"` - OnlyProxy bool `json:"onlyProxy"` + CurrentProfileName string `json:"currentProfileName"` + MixedPort int `json:"mixedPort"` + OnlyProxy bool `json:"onlyProxy"` } var state State diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 895241a..f518d46 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -100,22 +100,6 @@ class ClashCore { ); } - setProfileName(String profileName) { - final profileNameChar = profileName.toNativeUtf8().cast(); - clashFFI.setCurrentProfileName( - profileNameChar, - ); - malloc.free(profileNameChar); - } - - getProfileName() { - final currentProfileNameRaw = clashFFI.getCurrentProfileName(); - final currentProfileName = - currentProfileNameRaw.cast().toDartString(); - clashFFI.freeCString(currentProfileNameRaw); - return currentProfileName; - } - Future> getProxiesGroups() { final proxiesRaw = clashFFI.getProxies(); final proxiesRawString = proxiesRaw.cast().toDartString(); @@ -156,7 +140,8 @@ class ClashCore { clashFFI.freeCString(externalProvidersRaw); return Isolate.run>(() { final externalProviders = - (json.decode(externalProvidersRawString) as List) + (json.decode(externalProvidersRawString) as Map) + .values .map( (item) => ExternalProvider.fromJson(item), ) @@ -165,6 +150,18 @@ class ClashCore { }); } + ExternalProvider getExternalProvider(String externalProviderName) { + final externalProviderNameChar = + externalProviderName.toNativeUtf8().cast(); + final externalProviderRaw = + clashFFI.getExternalProvider(externalProviderNameChar); + malloc.free(externalProviderNameChar); + final externalProviderRawString = + externalProviderRaw.cast().toDartString(); + clashFFI.freeCString(externalProviderRaw); + return ExternalProvider.fromJson(json.decode(externalProviderRawString)); + } + Future updateExternalProvider({ required String providerName, required String providerType, @@ -216,21 +213,13 @@ class ClashCore { receiver.sendPort.nativePort, ); malloc.free(delayParamsChar); - Future.delayed(httpTimeoutDuration + moreDuration, () { - receiver.close(); - if (!completer.isCompleted) { - completer.complete( - Delay(name: proxyName, value: -1), - ); - } - }); return completer.future; } - clearEffect(String path) { - final pathChar = path.toNativeUtf8().cast(); - clashFFI.clearEffect(pathChar); - malloc.free(pathChar); + clearEffect(String profileId) { + final profileIdChar = profileId.toNativeUtf8().cast(); + clashFFI.clearEffect(profileIdChar); + malloc.free(profileIdChar); } VersionInfo getVersionInfo() { diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index f7c3778..e081ea5 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5190,30 +5190,6 @@ class ClashFFI { _lookup>('forceGc'); late final _forceGc = _forceGcPtr.asFunction(); - void setCurrentProfileName( - ffi.Pointer s, - ) { - return _setCurrentProfileName( - s, - ); - } - - late final _setCurrentProfileNamePtr = - _lookup)>>( - 'setCurrentProfileName'); - late final _setCurrentProfileName = _setCurrentProfileNamePtr - .asFunction)>(); - - ffi.Pointer getCurrentProfileName() { - return _getCurrentProfileName(); - } - - late final _getCurrentProfileNamePtr = - _lookup Function()>>( - 'getCurrentProfileName'); - late final _getCurrentProfileName = - _getCurrentProfileNamePtr.asFunction Function()>(); - void validateConfig( ffi.Pointer s, int port, @@ -5409,6 +5385,21 @@ class ClashFFI { late final _getExternalProviders = _getExternalProvidersPtr.asFunction Function()>(); + ffi.Pointer getExternalProvider( + ffi.Pointer name, + ) { + return _getExternalProvider( + name, + ); + } + + late final _getExternalProviderPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('getExternalProvider'); + late final _getExternalProvider = _getExternalProviderPtr + .asFunction Function(ffi.Pointer)>(); + void updateExternalProvider( ffi.Pointer providerName, ffi.Pointer providerType, diff --git a/lib/common/archive.dart b/lib/common/archive.dart new file mode 100644 index 0000000..b2c6630 --- /dev/null +++ b/lib/common/archive.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:path/path.dart'; + +extension ArchiveExt on Archive { + addDirectoryToArchive(String dirPath, String parentPath) { + final dir = Directory(dirPath); + final entities = dir.listSync(recursive: false); + for (final entity in entities) { + final relativePath = relative(entity.path, from: parentPath); + if (entity is File) { + final data = entity.readAsBytesSync(); + final archiveFile = ArchiveFile(relativePath, data.length, data); + addFile(archiveFile); + } else if (entity is Directory) { + addDirectoryToArchive(entity.path, parentPath); + } + } + } + + add(String name, T raw) { + final data = json.encode(raw); + addFile( + ArchiveFile(name, data.length, data), + ); + } +} diff --git a/lib/common/color.dart b/lib/common/color.dart index 30fe717..bdad4a4 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -16,6 +16,13 @@ extension ColorExtension on Color { toLittle() { return withOpacity(0.03); } + + Color darken([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(this); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + return hslDark.toColor(); + } } extension ColorSchemeExtension on ColorScheme { @@ -23,6 +30,7 @@ extension ColorSchemeExtension on ColorScheme { ? copyWith( surface: Colors.black, background: Colors.black, + surfaceContainer: surfaceContainer.darken(0.05), ) : this; } diff --git a/lib/common/constant.dart b/lib/common/constant.dart index a2d2e1d..a79a283 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -17,7 +17,7 @@ const mmdbFileName = "geoip.metadb"; const asnFileName = "ASN.mmdb"; const geoIpFileName = "GeoIP.dat"; const geoSiteFileName = "GeoSite.dat"; -final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0; +final double kHeaderHeight = system.isDesktop ? 40 : 0; const GeoXMap defaultGeoXMap = { "mmdb": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", diff --git a/lib/common/dav_client.dart b/lib/common/dav_client.dart index e546739..9056243 100644 --- a/lib/common/dav_client.dart +++ b/lib/common/dav_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -43,65 +44,16 @@ class DAVClient { get root => "/$appName"; - get remoteConfig => "$root/$configKey.json"; + get backupFile => "$root/backup.zip"; - get remoteClashConfig => "$root/$clashConfigKey.json"; - - get remoteProfiles => "$root/$profilesDirectoryName"; - - backup() async { - final appController = globalState.appController; - final config = appController.config; - final clashConfig = appController.clashConfig; + backup(Uint8List data) async { await client.mkdir("$root"); - client.write( - remoteConfig, - utf8.encode( - json.encode(config.toJson()), - ), - ); - client.write( - remoteClashConfig, - utf8.encode( - json.encode(clashConfig.toJson()), - ), - ); - await client.remove(remoteProfiles); - for (final profile in config.profiles) { - final path = await appPath.getProfilePath(profile.id); - if (path == null) continue; - await client.writeFromFile( - path, - "$remoteProfiles/${basename(path)}", - ); - } + await client.write("$backupFile", data); return true; } - recovery({required RecoveryOption recoveryOption}) async { - final profiles = await client.readDir(remoteProfiles); - final profilesPath = await appPath.getProfilesPath(); - for (final file in profiles) { - await client.read2File( - "$remoteProfiles/${file.name}", - join( - profilesPath, - file.name, - ), - ); - } - final configRaw = utf8.decode((await client.read(remoteConfig))); - final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig)); - final config = Config.fromJson(json.decode(configRaw)); - final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw)); - if(recoveryOption == RecoveryOption.onlyProfiles){ - globalState.appController.config.update(config, RecoveryOption.onlyProfiles); - }else{ - globalState.appController.config.update(config, RecoveryOption.all); - globalState.appController.clashConfig.update(clashConfig); - } - await globalState.appController.applyProfile(); - globalState.appController.savePreferences(); - return true; + Future> recovery() async { + final data = await client.read(backupFile); + return data; } } diff --git a/lib/common/navigation.dart b/lib/common/navigation.dart index 141b949..98c33f6 100644 --- a/lib/common/navigation.dart +++ b/lib/common/navigation.dart @@ -44,7 +44,7 @@ class Navigation { modes: [NavigationItemMode.desktop, NavigationItemMode.more], ), const NavigationItem( - icon: Icon(Icons.swap_vert_circle), + icon: Icon(Icons.storage), label: "resources", description: "resourcesDesc", keep: false, diff --git a/lib/common/other.dart b/lib/common/other.dart index 8f2ff76..f9746e6 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; @@ -208,6 +209,10 @@ class Other { int() => throw UnimplementedError(), }; } + + String getBackupFileName(){ + return "${appName}_backup_${DateTime.now().show}.zip"; + } } final other = Other(); diff --git a/lib/common/path.dart b/lib/common/path.dart index 25bb1ea..4945d60 100644 --- a/lib/common/path.dart +++ b/lib/common/path.dart @@ -9,6 +9,7 @@ import 'constant.dart'; class AppPath { static AppPath? _instance; Completer cacheDir = Completer(); + Completer downloadDir = Completer(); // Future _createDesktopCacheDir() async { // final path = join(dirname(Platform.resolvedExecutable), 'cache'); @@ -23,6 +24,9 @@ class AppPath { getApplicationSupportDirectory().then((value) { cacheDir.complete(value); }); + getDownloadsDirectory().then((value) { + downloadDir.complete(value); + }); // if (Platform.isAndroid) { // getApplicationSupportDirectory().then((value) { // cacheDir.complete(value); @@ -39,6 +43,11 @@ class AppPath { return _instance!; } + Future getDownloadDirPath() async { + final directory = await downloadDir.future; + return directory.path; + } + Future getHomeDirPath() async { final directory = await cacheDir.future; return directory.path; diff --git a/lib/common/picker.dart b/lib/common/picker.dart index 417e567..fa98382 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -1,22 +1,26 @@ +import 'dart:typed_data'; + import 'package:file_picker/file_picker.dart'; import 'package:fl_clash/common/common.dart'; import 'package:image_picker/image_picker.dart'; class Picker { - Future pickerConfigFile() async { + Future pickerFile() async { final filePickerResult = await FilePicker.platform.pickFiles( withData: true, allowMultiple: false, + initialDirectory: await appPath.getDownloadDirPath(), ); return filePickerResult?.files.first; } - Future pickerGeoDataFile() async { - final filePickerResult = await FilePicker.platform.pickFiles( - withData: true, - allowMultiple: false, + Future saveFile(String fileName,Uint8List bytes) async { + final path = await FilePicker.platform.saveFile( + fileName: fileName, + initialDirectory: await appPath.getDownloadDirPath(), + bytes: bytes, ); - return filePickerResult?.files.first; + return path; } Future pickerConfigQRCode() async { diff --git a/lib/common/window.dart b/lib/common/window.dart index 2e35d65..c4aa637 100644 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -20,6 +20,7 @@ class Window { WindowOptions windowOptions = WindowOptions( size: Size(props.width, props.height), minimumSize: const Size(380, 500), + windowButtonVisibility: false, titleBarStyle: TitleBarStyle.hidden, ); if (props.left != null || props.top != null) { diff --git a/lib/controller.dart b/lib/controller.dart index 9883d54..5f86d4f 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; +import 'package:archive/archive.dart'; +import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -90,9 +95,7 @@ class AppController { deleteProfile(String id) async { config.deleteProfileById(id); - final profilePath = await appPath.getProfilePath(id); - if (profilePath == null) return; - clashCore.clearEffect(profilePath); + clashCore.clearEffect(id); if (config.currentProfileId == id) { if (config.profiles.isNotEmpty) { final updateId = config.profiles.first.id; @@ -104,8 +107,10 @@ class AppController { } Future updateProfile(Profile profile) async { - await profile.update(); - config.setProfile(await profile.update()); + final newProfile = await profile.update(); + config.setProfile( + newProfile.copyWith(isUpdating: false), + ); } Future updateClashConfig({bool isPatch = true}) async { @@ -140,9 +145,6 @@ class AppController { changeProfile(String? value) async { if (value == config.currentProfileId) return; config.currentProfileId = value; - await applyProfile(); - appState.delayMap = {}; - saveConfigPreferences(); } autoUpdateProfiles() async { @@ -294,26 +296,6 @@ class AppController { if (!config.silentLaunch) { window?.show(); } - final commonScaffoldState = globalState.homeScaffoldKey.currentState; - if (commonScaffoldState?.mounted == true) { - await commonScaffoldState?.loadingRun(() async { - await globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - }, title: appLocalizations.init); - } else { - await globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - } - await afterInit(); - } - - afterInit() async { await proxyManager.updateStartTime(); if (proxyManager.isStart) { await updateSystemProxy(true); @@ -403,7 +385,7 @@ class AppController { } addProfileFormFile() async { - final platformFile = await globalState.safeRun(picker.pickerConfigFile); + final platformFile = await globalState.safeRun(picker.pickerFile); final bytes = platformFile?.bytes; if (bytes == null) { return null; @@ -480,4 +462,63 @@ class AppController { config.currentSelectedMap[groupName] ?? '') ?? ''; } + + Future> backupData() async { + final homeDirPath = await appPath.getHomeDirPath(); + final profilesPath = await appPath.getProfilesPath(); + final configJson = config.toJson(); + final clashConfigJson = clashConfig.toJson(); + return Isolate.run>(() async { + final archive = Archive(); + archive.add("config.json", configJson); + archive.add("clashConfig.json", clashConfigJson); + await archive.addDirectoryToArchive(profilesPath, homeDirPath); + final zipEncoder = ZipEncoder(); + return zipEncoder.encode(archive) ?? []; + }); + } + + recoveryData( + List data, + RecoveryOption recoveryOption, + ) async { + final archive = await Isolate.run(() { + final zipDecoder = ZipDecoder(); + return zipDecoder.decodeBytes(data); + }); + final homeDirPath = await appPath.getHomeDirPath(); + final configs = + archive.files.where((item) => item.name.endsWith(".json")).toList(); + final profiles = + archive.files.where((item) => !item.name.endsWith(".json")); + final configIndex = + configs.indexWhere((config) => config.name == "config.json"); + final clashConfigIndex = + configs.indexWhere((config) => config.name == "clashConfig.json"); + if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip"; + final configFile = configs[configIndex]; + final clashConfigFile = configs[clashConfigIndex]; + final tempConfig = Config.fromJson( + json.decode( + utf8.decode(configFile.content), + ), + ); + final tempClashConfig = ClashConfig.fromJson( + json.decode( + utf8.decode(clashConfigFile.content), + ), + ); + for (final profile in profiles) { + final filePath = join(homeDirPath, profile.name); + final file = File(filePath); + await file.create(recursive: true); + await file.writeAsBytes(profile.content); + } + if (recoveryOption == RecoveryOption.onlyProfiles) { + config.update(tempConfig, RecoveryOption.onlyProfiles); + } else { + config.update(tempConfig, RecoveryOption.all); + clashConfig.update(tempClashConfig); + } + } } diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 4054d6a..6d1b493 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -23,24 +23,19 @@ class AccessFragment extends StatefulWidget { } class _AccessFragmentState extends State { - final packagesListenable = ValueNotifier>([]); - @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(const Duration(milliseconds: 300), () async { - packagesListenable.value = await app?.getPackages() ?? []; - }); + final appState = globalState.appController.appState; + if (appState.packages.isEmpty) { + Future.delayed(const Duration(milliseconds: 300), () async { + appState.packages = await app?.getPackages() ?? []; + }); + } }); } - @override - void dispose() { - super.dispose(); - packagesListenable.dispose(); - } - Widget _buildAppProxyModePopup() { final items = [ CommonPopupMenuItem( @@ -156,8 +151,8 @@ class _AccessFragmentState extends State { // } Widget _buildPackageList() { - return ValueListenableBuilder( - valueListenable: packagesListenable, + return Selector>( + selector: (_, appState) => appState.packages, builder: (_, packages, ___) { final accessControl = globalState.appController.config.accessControl; final acceptList = accessControl.acceptList; @@ -238,10 +233,10 @@ class _AccessFragmentState extends State { .textTheme .labelLarge ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), + color: Theme.of(context) + .colorScheme + .primary, + ), ), ), const Flexible( @@ -256,10 +251,10 @@ class _AccessFragmentState extends State { .textTheme .labelLarge ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), + color: Theme.of(context) + .colorScheme + .primary, + ), ), ), ], @@ -288,47 +283,43 @@ class _AccessFragmentState extends State { ), Expanded( flex: 1, - child: FadeBox( - key: const Key("fade_box"), - child: currentPackages.isEmpty - ? const Center( - child: CircularProgressIndicator(), - ) - : ListView.builder( - itemCount: currentPackages.length, - itemBuilder: (_, index) { - final package = currentPackages[index]; - return PackageListItem( - key: Key(package.packageName), - package: package, - value: - valueList.contains(package.packageName), - isActive: isAccessControl, - onChanged: (value) { - if (value == true) { - valueList.add(package.packageName); - } else { - valueList.remove(package.packageName); - } - final config = - globalState.appController.config; - if (accessControlMode == - AccessControlMode.acceptSelected) { - config.accessControl = - config.accessControl.copyWith( + child: currentPackages.isEmpty + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: currentPackages.length, + itemBuilder: (_, index) { + final package = currentPackages[index]; + return PackageListItem( + key: Key(package.packageName), + package: package, + value: valueList.contains(package.packageName), + isActive: isAccessControl, + onChanged: (value) { + if (value == true) { + valueList.add(package.packageName); + } else { + valueList.remove(package.packageName); + } + final config = + globalState.appController.config; + if (accessControlMode == + AccessControlMode.acceptSelected) { + config.accessControl = + config.accessControl.copyWith( acceptList: valueList, ); - } else { - config.accessControl = - config.accessControl.copyWith( + } else { + config.accessControl = + config.accessControl.copyWith( rejectList: valueList, ); - } + } + }, + ); }, - ); - }, - ), - ), + ), ), ], ), diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index 2d30688..cb6c2fe 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -1,25 +1,22 @@ +import 'dart:io'; +import 'dart:typed_data'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/dav_client.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/dav.dart'; import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class BackupAndRecovery extends StatefulWidget { +class BackupAndRecovery extends StatelessWidget { const BackupAndRecovery({super.key}); - @override - State createState() => _BackupAndRecoveryState(); -} - -class _BackupAndRecoveryState extends State { - DAVClient? _client; - _showAddWebDAV(DAV? dav) async { await globalState.showCommonDialog( child: WebDAVFormDialog( @@ -28,11 +25,15 @@ class _BackupAndRecoveryState extends State { ); } - _backup() async { + _backupOnWebDAV(BuildContext context, DAVClient client) async { final commonScaffoldState = context.commonScaffoldState; - final res = await commonScaffoldState?.loadingRun(() async { - return await _client?.backup(); - }); + final res = await commonScaffoldState?.loadingRun( + () async { + final backupData = await globalState.appController.backupData(); + return await client.backup(Uint8List.fromList(backupData)); + }, + title: appLocalizations.backup, + ); if (res != true) return; globalState.showMessage( title: appLocalizations.backup, @@ -40,11 +41,20 @@ class _BackupAndRecoveryState extends State { ); } - _recovery(RecoveryOption recoveryOption) async { + _recoveryOnWebDAV( + BuildContext context, + DAVClient client, + RecoveryOption recoveryOption, + ) async { final commonScaffoldState = context.commonScaffoldState; - final res = await commonScaffoldState?.loadingRun(() async { - return await _client?.recovery(recoveryOption: recoveryOption); - }); + final res = await commonScaffoldState?.loadingRun( + () async { + final data = await client.recovery(); + await globalState.appController.recoveryData(data, recoveryOption); + return true; + }, + title: appLocalizations.recovery, + ); if (res != true) return; globalState.showMessage( title: appLocalizations.recovery, @@ -52,12 +62,65 @@ class _BackupAndRecoveryState extends State { ); } - _handleRecovery() async { + _handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async { final recoveryOption = await globalState.showCommonDialog( child: const RecoveryOptionsDialog(), ); - if (recoveryOption == null) return; - _recovery(recoveryOption); + if (recoveryOption == null || !context.mounted) return; + _recoveryOnWebDAV(context, client, recoveryOption); + } + + _backupOnLocal(BuildContext context) async { + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun( + () async { + final backupData = await globalState.appController.backupData(); + await picker.saveFile( + other.getBackupFileName(), + Uint8List.fromList(backupData), + ); + return true; + }, + title: appLocalizations.backup, + ); + if (res != true) return; + globalState.showMessage( + title: appLocalizations.backup, + message: TextSpan(text: appLocalizations.backupSuccess), + ); + } + + _recoveryOnLocal( + BuildContext context, + RecoveryOption recoveryOption, + ) async { + final file = await picker.pickerFile(); + final data = file?.bytes; + if (data == null || !context.mounted) return; + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun( + () async { + await globalState.appController.recoveryData( + List.from(data), + recoveryOption, + ); + return true; + }, + title: appLocalizations.recovery, + ); + if (res != true) return; + globalState.showMessage( + title: appLocalizations.recovery, + message: TextSpan(text: appLocalizations.recoverySuccess), + ); + } + + _handleRecoveryOnLocal(BuildContext context) async { + final recoveryOption = await globalState.showCommonDialog( + child: const RecoveryOptionsDialog(), + ); + if (recoveryOption == null || !context.mounted) return; + _recoveryOnLocal(context, recoveryOption); } @override @@ -65,12 +128,11 @@ class _BackupAndRecoveryState extends State { return Selector( selector: (_, config) => config.dav, builder: (_, dav, __) { - if (dav == null) { - return ListView( - children: [ - ListHeader( - title: appLocalizations.account, - ), + final client = dav != null ? DAVClient(dav) : null; + return ListView( + children: [ + ListHeader(title: appLocalizations.remote), + if (dav == null) ListItem( leading: const Icon(Icons.account_box), title: Text(appLocalizations.noInfo), @@ -83,95 +145,95 @@ class _BackupAndRecoveryState extends State { appLocalizations.bind, ), ), + ) + else ...[ + ListItem( + leading: const Icon(Icons.account_box), + title: TooltipText( + text: Text( + dav.user, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(appLocalizations.connectivity), + FutureBuilder( + future: client!.pingCompleter.future, + builder: (_, snapshot) { + return Center( + child: FadeBox( + child: snapshot.connectionState == + ConnectionState.waiting + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + : Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: snapshot.data == true + ? Colors.green + : Colors.red, + ), + width: 12, + height: 12, + ), + ), + ); + }, + ), + ], + ), + ), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); + }, + child: Text( + appLocalizations.edit, + ), + ), + ), + const SizedBox( + height: 4, + ), + ListItem( + onTap: () { + _backupOnWebDAV(context, client); + }, + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.remoteBackupDesc), + ), + ListItem( + onTap: () { + _handleRecoveryOnWebDAV(context, client); + }, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.remoteRecoveryDesc), ), ], - ); - } - _client = DAVClient(dav); - final pingFuture = _client!.pingCompleter.future; - return ListView( - children: [ - ListHeader(title: appLocalizations.account), + ListHeader(title: appLocalizations.local), ListItem( - leading: const Icon(Icons.account_box), - title: TooltipText( - text: Text( - dav.user, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(appLocalizations.connectivity), - FutureBuilder( - future: pingFuture, - builder: (_, snapshot) { - return Center( - child: FadeBox( - key: const Key("fade_box_1"), - child: snapshot.connectionState == ConnectionState.waiting - ? const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - strokeWidth: 1, - ), - ) - : Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: snapshot.data == true - ? Colors.green - : Colors.red, - ), - width: 12, - height: 12, - ), - ), - ); - }, - ), - ], - ), - ), - trailing: FilledButton.tonal( - onPressed: () { - _showAddWebDAV(dav); - }, - child: Text( - appLocalizations.edit, - ), - ), - ), - FutureBuilder( - future: pingFuture, - builder: (_, snapshot) { - return FadeBox( - key: const Key("fade_box_2"), - child: snapshot.data == true - ? Column( - children: [ - ListHeader( - title: appLocalizations.backupAndRecovery), - ListItem( - onTap: _backup, - title: Text(appLocalizations.backup), - subtitle: Text(appLocalizations.backupDesc), - ), - ListItem( - onTap: _handleRecovery, - title: Text(appLocalizations.recovery), - subtitle: Text(appLocalizations.recoveryDesc), - ), - ], - ) - : Container(), - ); + onTap: () { + _backupOnLocal(context); }, + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.localBackupDesc), + ), + ListItem( + onTap: () { + _handleRecoveryOnLocal(context); + }, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.localRecoveryDesc), ), ], ); @@ -180,6 +242,50 @@ class _BackupAndRecoveryState extends State { } } +class RecoveryOptionsDialog extends StatefulWidget { + const RecoveryOptionsDialog({super.key}); + + @override + State createState() => _RecoveryOptionsDialogState(); +} + +class _RecoveryOptionsDialogState extends State { + _handleOnTab(RecoveryOption? value) { + if (value == null) return; + Navigator.of(context).pop(value); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.recovery), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.onlyProfiles); + }, + title: Text(appLocalizations.recoveryProfiles), + ), + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.all); + }, + title: Text(appLocalizations.recoveryAll), + ) + ], + ), + ), + ); + } +} + class WebDAVFormDialog extends StatefulWidget { final DAV? dav; @@ -238,7 +344,7 @@ class _WebDAVFormDialogState extends State { children: [ TextFormField( controller: uriController, - maxLines: 2, + maxLines: 5, minLines: 1, decoration: InputDecoration( prefixIcon: const Icon(Icons.link), @@ -313,47 +419,3 @@ class _WebDAVFormDialogState extends State { ); } } - -class RecoveryOptionsDialog extends StatefulWidget { - const RecoveryOptionsDialog({super.key}); - - @override - State createState() => _RecoveryOptionsDialogState(); -} - -class _RecoveryOptionsDialogState extends State { - _handleOnTab(RecoveryOption? value) { - if (value == null) return; - Navigator.of(context).pop(value); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.recovery), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.onlyProfiles); - }, - title: Text(appLocalizations.recoveryProfiles), - ), - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.all); - }, - title: Text(appLocalizations.recoveryAll), - ) - ], - ), - ), - ); - } -} diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index cddf298..19438de 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -137,11 +137,40 @@ class _ConfigFragmentState extends State { } } + _updateKeepAliveInterval(int keepAliveInterval) async { + final newKeepAliveIntervalString = + await globalState.showCommonDialog( + child: KeepAliveIntervalFormDialog( + keepAliveInterval: keepAliveInterval, + ), + ); + if (newKeepAliveIntervalString != null && + newKeepAliveIntervalString != "$keepAliveInterval" && + mounted) { + try { + final newKeepAliveInterval = int.parse(newKeepAliveIntervalString); + if (newKeepAliveInterval <= 0) { + throw "Invalid keepAliveInterval"; + } + globalState.appController.clashConfig.keepAliveInterval = + newKeepAliveInterval; + globalState.appController.updateClashConfigDebounce(); + } catch (e) { + globalState.showMessage( + title: appLocalizations.testUrl, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + } + List _buildAppSection() { return generateSection( title: appLocalizations.app, items: [ - if (Platform.isAndroid)...[ + if (Platform.isAndroid) ...[ Selector( selector: (_, config) => config.allowBypass, builder: (_, allowBypass, __) { @@ -263,6 +292,19 @@ class _ConfigFragmentState extends State { ); }, ), + Selector( + selector: (_, config) => config.keepAliveInterval, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.timer_outlined), + title: Text(appLocalizations.keepAliveIntervalDesc), + subtitle: Text("$value ${appLocalizations.seconds}"), + onTap: () { + _updateKeepAliveInterval(value); + }, + ); + }, + ), Selector( selector: (_, config) => config.testUrl, builder: (_, value, __) { @@ -589,3 +631,64 @@ class _TestUrlFormDialogState extends State { ); } } + +class KeepAliveIntervalFormDialog extends StatefulWidget { + final int keepAliveInterval; + + const KeepAliveIntervalFormDialog({ + super.key, + required this.keepAliveInterval, + }); + + @override + State createState() => + _KeepAliveIntervalFormDialogState(); +} + +class _KeepAliveIntervalFormDialogState + extends State { + late TextEditingController keepAliveIntervalController; + + @override + void initState() { + super.initState(); + keepAliveIntervalController = + TextEditingController(text: "${widget.keepAliveInterval}"); + } + + _handleUpdate() async { + final keepAliveInterval = keepAliveIntervalController.value.text; + if (keepAliveInterval.isEmpty) return; + Navigator.of(context).pop(keepAliveInterval); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.keepAliveIntervalDesc), + content: SizedBox( + width: 300, + child: Wrap( + runSpacing: 16, + children: [ + TextField( + maxLines: 1, + minLines: 1, + controller: keepAliveIntervalController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixText: appLocalizations.seconds, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _handleUpdate, + child: Text(appLocalizations.submit), + ) + ], + ); + } +} diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 8bcfee7..0a53ace 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -11,6 +11,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'view_profile.dart'; + class EditProfile extends StatefulWidget { final Profile profile; final BuildContext context; @@ -121,7 +123,7 @@ class _EditProfileState extends State { } _uploadProfileFile() async { - final platformFile = await globalState.safeRun(picker.pickerConfigFile); + final platformFile = await globalState.safeRun(picker.pickerFile); if (platformFile?.bytes == null) return; fileData = platformFile?.bytes; fileInfoNotifier.value = fileInfoNotifier.value?.copyWith( @@ -260,23 +262,19 @@ class _EditProfileState extends State { padding: const EdgeInsets.symmetric( vertical: 16, ), - child: ScrollOverBuilder( - builder: (isOver) { - return ListView.separated( - padding: kMaterialListPadding.copyWith( - bottom: isOver ? 72 : 36, - ), - itemBuilder: (_, index) { - return items[index]; - }, - separatorBuilder: (_, __) { - return const SizedBox( - height: 24, - ); - }, - itemCount: items.length, + child: ListView.separated( + padding: kMaterialListPadding.copyWith( + bottom: 72, + ), + itemBuilder: (_, index) { + return items[index]; + }, + separatorBuilder: (_, __) { + return const SizedBox( + height: 24, ); }, + itemCount: items.length, ), ), ), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 0c70c8d..6113219 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -27,8 +27,6 @@ class ProfilesFragment extends StatefulWidget { class _ProfilesFragmentState extends State { Function? applyConfigDebounce; - List> profileItemKeys = []; - _handleShowAddExtendPage() { showExtendPage( globalState.navigatorKey.currentState!.context, @@ -51,8 +49,28 @@ class _ProfilesFragmentState extends State { } _updateProfiles() async { - final updateProfiles = profileItemKeys.map( - (key) async => await key.currentState?.updateProfile(false)); + final appController = globalState.appController; + final config = appController.config; + final profiles = appController.config.profiles; + final updateProfiles = profiles.map( + (profile) async { + config.setProfile( + profile.copyWith(isUpdating: true), + ); + try { + await appController.updateProfile(profile); + if (profile.id == appController.config.currentProfile?.id) { + appController.applyProfile(isPrue: true); + } + } catch (_) { + config.setProfile( + profile.copyWith( + isUpdating: false, + ), + ); + } + }, + ); await Future.wait(updateProfiles); } @@ -74,19 +92,6 @@ class _ProfilesFragmentState extends State { ); } - _changeProfile(String? id) async { - final appController = globalState.appController; - final config = appController.config; - if (id == config.currentProfileId) return; - config.currentProfileId = id; - applyConfigDebounce ??= debounce(() async { - await appController.applyProfile(); - appController.appState.delayMap = {}; - appController.saveConfigPreferences(); - }); - applyConfigDebounce!(); - } - @override Widget build(BuildContext context) { return FloatLayout( @@ -119,40 +124,33 @@ class _ProfilesFragmentState extends State { label: appLocalizations.nullProfileDesc, ); } - profileItemKeys = state.profiles - .map( - (profile) => GlobalObjectKey<_ProfileItemState>(profile.id)) - .toList(); final columns = _getColumns(state.viewMode); return Align( alignment: Alignment.topCenter, - child: ScrollOverBuilder( - builder: (isOver) { - return SingleChildScrollView( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: 16 + (isOver ? 72 : 0), - ), - child: Grid( - mainAxisSpacing: 16, - crossAxisSpacing: 16, - crossAxisCount: columns, - children: [ - for (int i = 0; i < state.profiles.length; i++) - GridItem( - child: ProfileItem( - key: profileItemKeys[i], - profile: state.profiles[i], - groupValue: state.currentProfileId, - onChanged: _changeProfile, - ), - ), - ], - ), - ); - }, + child: SingleChildScrollView( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 88, + ), + child: Grid( + mainAxisSpacing: 16, + crossAxisSpacing: 16, + crossAxisCount: columns, + children: [ + for (int i = 0; i < state.profiles.length; i++) + GridItem( + child: ProfileItem( + key: Key(state.profiles[i].id), + profile: state.profiles[i], + groupValue: state.currentProfileId, + onChanged: + globalState.appController.changeProfile, + ), + ), + ], + ), ), ); }, @@ -162,7 +160,7 @@ class _ProfilesFragmentState extends State { } } -class ProfileItem extends StatefulWidget { +class ProfileItem extends StatelessWidget { final Profile profile; final String? groupValue; final void Function(String? value) onChanged; @@ -174,22 +172,15 @@ class ProfileItem extends StatefulWidget { required this.onChanged, }); - @override - State createState() => _ProfileItemState(); -} - -class _ProfileItemState extends State { - final isUpdating = ValueNotifier(false); - - _handleDeleteProfile() async { + _handleDeleteProfile(BuildContext context) async { globalState.showMessage( title: appLocalizations.tip, message: TextSpan( text: appLocalizations.deleteProfileTip, ), onTab: () async { - await globalState.appController.deleteProfile(widget.profile.id); - if (mounted) { + await globalState.appController.deleteProfile(profile.id); + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -200,41 +191,47 @@ class _ProfileItemState extends State { await globalState.safeRun(updateProfile); } - Future updateProfile([isSingle = true]) async { - isUpdating.value = true; - try { - final appController = globalState.appController; - await appController.updateProfile(widget.profile); - if (widget.profile.id == appController.config.currentProfile?.id) { - globalState.appController.applyProfile(isPrue: true); - } - } catch (e) { - isUpdating.value = false; - if (!isSingle) { - return e.toString(); - } else { + Future updateProfile() async { + final appController = globalState.appController; + final config = appController.config; + if (profile.type == ProfileType.file) return; + await globalState.safeRun(() async { + try { + config.setProfile( + profile.copyWith( + isUpdating: true, + ), + ); + await appController.updateProfile(profile); + if (profile.id == appController.config.currentProfile?.id) { + appController.applyProfile(isPrue: true); + } + } catch (e) { + config.setProfile( + profile.copyWith( + isUpdating: false, + ), + ); rethrow; } - } - isUpdating.value = false; - return null; + }); } - _handleShowEditExtendPage() { + _handleShowEditExtendPage(BuildContext context) { showExtendPage( context, body: EditProfile( - profile: widget.profile, + profile: profile, context: context, ), title: "${appLocalizations.edit}${appLocalizations.profile}", ); } - List _buildUserInfo(UserInfo userInfo) { + List _buildUserInfo(BuildContext context, UserInfo userInfo) { final use = userInfo.upload + userInfo.download; final total = userInfo.total; - if(total == 0){ + if (total == 0) { return []; } final useShow = TrafficValue(value: use).show; @@ -261,13 +258,13 @@ class _ProfileItemState extends State { ]; } - List _buildUrlProfileInfo(Profile profile) { + List _buildUrlProfileInfo(BuildContext context) { final userInfo = profile.userInfo; return [ const SizedBox( height: 8, ), - if (userInfo != null) ..._buildUserInfo(userInfo), + if (userInfo != null) ..._buildUserInfo(context, userInfo), Text( profile.lastUpdateDate?.lastUpdateTimeDesc ?? "", style: context.textTheme.labelMedium?.toLight, @@ -275,7 +272,7 @@ class _ProfileItemState extends State { ]; } - List _buildFileProfileInfo(Profile profile) { + List _buildFileProfileInfo(BuildContext context) { return [ const SizedBox( height: 8, @@ -287,50 +284,8 @@ class _ProfileItemState extends State { ]; } - _buildTitle(Profile profile) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - profile.label ?? profile.id, - style: context.textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...switch (profile.type) { - ProfileType.file => _buildFileProfileInfo( - profile, - ), - ProfileType.url => _buildUrlProfileInfo( - profile, - ), - }, - ], - ), - ], - ), - ); - } - - @override - void dispose() { - isUpdating.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - final profile = widget.profile; - final groupValue = widget.groupValue; - final onChanged = widget.onChanged; return CommonCard( isSelected: profile.id == groupValue, onPressed: () { @@ -343,55 +298,75 @@ class _ProfileItemState extends State { trailing: SizedBox( height: 40, width: 40, - child: ValueListenableBuilder( - valueListenable: isUpdating, - builder: (_, isUpdating, ___) { - return FadeBox( - child: isUpdating - ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : CommonPopupMenu( - items: [ - CommonPopupMenuItem( - action: ProfileActions.edit, - label: appLocalizations.edit, - iconData: Icons.edit, - ), - if (profile.type == ProfileType.url) - CommonPopupMenuItem( - action: ProfileActions.update, - label: appLocalizations.update, - iconData: Icons.sync, - ), - CommonPopupMenuItem( - action: ProfileActions.delete, - label: appLocalizations.delete, - iconData: Icons.delete, - ), - ], - onSelected: (ProfileActions? action) async { - switch (action) { - case ProfileActions.edit: - _handleShowEditExtendPage(); - break; - case ProfileActions.delete: - _handleDeleteProfile(); - break; - case ProfileActions.update: - _handleUpdateProfile(); - break; - case null: - break; - } - }, + child: FadeBox( + child: profile.isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : CommonPopupMenu( + items: [ + CommonPopupMenuItem( + action: ProfileActions.edit, + label: appLocalizations.edit, + iconData: Icons.edit, ), - ); - }, + if (profile.type == ProfileType.url) + CommonPopupMenuItem( + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, + ), + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(context); + break; + case ProfileActions.delete: + _handleDeleteProfile(context); + break; + case ProfileActions.update: + _handleUpdateProfile(); + break; + case null: + break; + } + }, + ), + ), + ), + title: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + profile.label ?? profile.id, + style: context.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...switch (profile.type) { + ProfileType.file => _buildFileProfileInfo(context), + ProfileType.url => _buildUrlProfileInfo(context), + }, + ], + ), + ], ), ), - title: _buildTitle(profile), tileTitleAlignment: ListTileTitleAlignment.titleHeight, ), ); diff --git a/lib/fragments/profiles/view_profile.dart b/lib/fragments/profiles/view_profile.dart new file mode 100644 index 0000000..e1b42d8 --- /dev/null +++ b/lib/fragments/profiles/view_profile.dart @@ -0,0 +1,232 @@ +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:re_editor/re_editor.dart'; +import 'package:re_highlight/languages/yaml.dart'; +import 'package:re_highlight/styles/atom-one-light.dart'; + +class ViewProfile extends StatefulWidget { + final Profile profile; + + const ViewProfile({ + super.key, + required this.profile, + }); + + @override + State createState() => _ViewProfileState(); +} + +class _ViewProfileState extends State { + bool readOnly = true; + final CodeLineEditingController _controller = CodeLineEditingController(); + final key = GlobalKey(); + final _focusNode = FocusNode(); + String? rawText; + + @override + void initState() { + super.initState(); + appPath.getProfilePath(widget.profile.id).then((path) async { + if (path == null) return; + final file = File(path); + rawText = await file.readAsString(); + _controller.text = rawText ?? ""; + }); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + _focusNode.dispose(); + } + + Profile get profile => widget.profile; + + _handleChangeReadOnly() async { + if (readOnly == true) { + setState(() { + readOnly = false; + }); + } else { + if (_controller.text == rawText) return; + final newProfile = await key.currentState?.loadingRun(() async { + return await profile.saveFileWithString(_controller.text); + }); + if (newProfile == null) return; + globalState.appController.config.setProfile(newProfile); + setState(() { + readOnly = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return CommonScaffold( + key: key, + actions: [ + IconButton( + onPressed: _controller.undo, + icon: const Icon(Icons.undo), + ), + IconButton( + onPressed: _controller.redo, + icon: const Icon(Icons.redo), + ), + IconButton( + onPressed: _handleChangeReadOnly, + icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save), + ), + ], + body: CodeEditor( + readOnly: readOnly, + focusNode: _focusNode, + scrollbarBuilder: (context, child, details) { + return Scrollbar( + controller: details.controller, + thickness: 8, + radius: const Radius.circular(2), + interactive: true, + child: child, + ); + }, + showCursorWhenReadOnly: false, + controller: _controller, + shortcutsActivatorsBuilder: + const DefaultCodeShortcutsActivatorsBuilder(), + indicatorBuilder: ( + context, + editingController, + chunkController, + notifier, + ) { + return Row( + children: [ + DefaultCodeLineNumber( + controller: editingController, + notifier: notifier, + ), + DefaultCodeChunkIndicator( + width: 20, + controller: chunkController, + notifier: notifier, + ) + ], + ); + }, + toolbarController: + !readOnly ? ContextMenuControllerImpl(_focusNode) : null, + style: CodeEditorStyle( + fontSize: 14, + codeTheme: CodeHighlightTheme( + languages: { + 'yaml': CodeHighlightThemeMode( + mode: langYaml, + ) + }, + theme: atomOneLightTheme, + ), + ), + ), + title: widget.profile.label ?? widget.profile.id, + ); + } +} + +class ContextMenuItemWidget extends PopupMenuItem { + ContextMenuItemWidget({ + super.key, + required String text, + required VoidCallback super.onTap, + }) : super(child: Text(text)); +} + +class ContextMenuControllerImpl implements SelectionToolbarController { + OverlayEntry? _overlayEntry; + + final FocusNode focusNode; + + ContextMenuControllerImpl( + this.focusNode, + ); + + _removeOverLayEntry() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + @override + void hide(BuildContext context) { + // _removeOverLayEntry(); + } + + _handleCut(CodeLineEditingController controller) { + controller.cut(); + _removeOverLayEntry(); + } + + _handleCopy(CodeLineEditingController controller) async { + await controller.copy(); + _removeOverLayEntry(); + } + + _handlePaste(CodeLineEditingController controller) { + controller.paste(); + _removeOverLayEntry(); + } + + @override + void show({ + required BuildContext context, + required CodeLineEditingController controller, + required TextSelectionToolbarAnchors anchors, + Rect? renderRect, + required LayerLink layerLink, + required ValueNotifier visibility, + }) { + if (controller.selectedText.isEmpty) { + return; + } + _removeOverLayEntry(); + final relativeRect = RelativeRect.fromSize( + (anchors.primaryAnchor) & + const Size(150, double.infinity), + MediaQuery.of(context).size, + ); + _overlayEntry ??= OverlayEntry( + builder: (context) => ValueListenableBuilder( + valueListenable: controller, + builder: (_, __, child) { + if (controller.selectedText.isEmpty) { + _removeOverLayEntry(); + } + return child!; + }, + child: Positioned( + left: relativeRect.left, + top: relativeRect.top, + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(focusNode); + }, + child: Container( + width: 200, + height: 200, + color: Colors.green, + ), + ), + ), + ), + ), + ); + Overlay.of(context).insert(_overlayEntry!); + } +} diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index 629d0dd..46dfca6 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -42,7 +42,7 @@ double getItemHeight(ProxyCardType proxyCardType) { delayTest(List proxies) async { final appController = globalState.appController; - for (final proxy in proxies) { + final delayProxies = proxies.map((proxy) async { final proxyName = appController.appState.getRealProxyName(proxy.name); globalState.appController.setDelay( Delay( @@ -50,11 +50,9 @@ delayTest(List proxies) async { value: 0, ), ); - clashCore.getDelay(proxyName).then((delay) { - globalState.appController.setDelay(delay); - }); - } - await Future.delayed(httpTimeoutDuration + moreDuration); + globalState.appController.setDelay(await clashCore.getDelay(proxyName)); + }); + await Future.wait(delayProxies); appController.appState.sortNum++; } diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart new file mode 100644 index 0000000..dd439cf --- /dev/null +++ b/lib/fragments/proxies/providers.dart @@ -0,0 +1,187 @@ +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/app.dart'; +import 'package:fl_clash/models/ffi.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef UpdatingMap = Map; + +class Providers extends StatefulWidget { + const Providers({ + super.key, + }); + + @override + State createState() => _ProvidersState(); +} + +class _ProvidersState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + _updateProviders(); + }, + icon: const Icon( + Icons.sync, + ), + ) + ]; + }, + ); + } + + _updateProviders() async { + final appState = globalState.appController.appState; + final providers = globalState.appController.appState.providers; + final updateProviders = providers.map( + (provider) async { + appState.setProvider( + provider.copyWith(isUpdating: true), + ); + await clashCore.updateExternalProvider( + providerName: provider.name, + providerType: provider.type, + ); + appState.setProvider( + clashCore.getExternalProvider(provider.name), + ); + }, + ); + await Future.wait(updateProviders); + } + + @override + Widget build(BuildContext context) { + return Selector>( + selector: (_, appState) => appState.providers, + builder: (_, providers, ___) { + return ListView.separated( + itemBuilder: (_, index) { + return ProviderItem( + provider: providers[index], + ); + }, + separatorBuilder: (_, index) { + return const Divider( + height: 0, + ); + }, + itemCount: providers.length, + ); + }, + ); + } +} + +class ProviderItem extends StatelessWidget { + final ExternalProvider provider; + + const ProviderItem({ + super.key, + required this.provider, + }); + + _handleUpdateProfile() async { + await globalState.safeRun(updateProvider); + } + + updateProvider() async { + final appState = globalState.appController.appState; + if (provider.vehicleType != "HTTP") return; + await globalState.safeRun(() async { + appState.setProvider( + provider.copyWith( + isUpdating: true, + ), + ); + final message = await clashCore.updateExternalProvider( + providerName: provider.name, + providerType: provider.type, + ); + if (message.isNotEmpty) throw message; + }); + appState.setProvider( + clashCore.getExternalProvider(provider.name), + ); + } + + String _buildProviderDesc() { + final baseInfo = + "${provider.type}(${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}"; + final count = provider.count; + return switch (count == 0) { + true => baseInfo, + false => "$baseInfo · $count${appLocalizations.entries}", + }; + } + + @override + Widget build(BuildContext context) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + title: Text(provider.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + Text( + _buildProviderDesc(), + ), + Text( + provider.path, + style: context.textTheme.bodyMedium?.toLight, + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 12, + children: [ + // CommonChip( + // avatar: const Icon(Icons.upload), + // label: appLocalizations.upload, + // onPressed: () {}, + // ), + if (provider.vehicleType == "HTTP") + CommonChip( + avatar: const Icon(Icons.sync), + label: appLocalizations.sync, + onPressed: () { + _handleUpdateProfile(); + }, + ), + ], + ), + ], + ), + trailing: SizedBox( + height: 48, + width: 48, + child: FadeBox( + child: provider.isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : const SizedBox(), + ), + ), + ); + } +} diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index 2ce2b28..31ee953 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -6,6 +6,7 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'providers.dart'; import 'setting.dart'; import 'tab.dart'; @@ -19,18 +20,37 @@ class ProxiesFragment extends StatefulWidget { class _ProxiesFragmentState extends State { final GlobalKey _proxiesTabKey = GlobalKey(); - _initActions(ProxiesType proxiesType) { + _initActions(ProxiesType proxiesType, bool hasProvider) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.actions = [ + if (hasProvider) ...[ + IconButton( + onPressed: () { + showExtendPage( + forceNotSide: true, + extendPageWidth: 360, + context, + body: const Providers(), + title: appLocalizations.externalResources, + ); + }, + icon: const Icon( + Icons.swap_vert_circle_outlined, + ), + ), + const SizedBox( + width: 8, + ), + ], if (proxiesType == ProxiesType.tab) ...[ IconButton( onPressed: () { _proxiesTabKey.currentState?.scrollToGroupSelected(); }, icon: const Icon( - Icons.gps_fixed, + Icons.adjust_outlined, ), ), const SizedBox( @@ -60,18 +80,18 @@ class _ProxiesFragmentState extends State { return Selector( selector: (_, config) => config.proxiesType, builder: (_, proxiesType, __) { - return Selector( - selector: (_, appState) => appState.currentLabel == 'proxies', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initActions(proxiesType); + return ProxiesActionsBuilder( + builder: (state, child) { + if (state.isCurrent) { + _initActions(proxiesType, state.hasProvider); } - return switch (proxiesType) { - ProxiesType.tab => ProxiesTabFragment( - key: _proxiesTabKey, - ), - ProxiesType.list => const ProxiesListFragment(), - }; + return child!; + }, + child: switch (proxiesType) { + ProxiesType.tab => ProxiesTabFragment( + key: _proxiesTabKey, + ), + ProxiesType.list => const ProxiesListFragment(), }, ); }, diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 0bbda9d..d6a651f 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -22,80 +22,11 @@ class GeoItem { }); } -class Resources extends StatefulWidget { +class Resources extends StatelessWidget { const Resources({super.key}); @override - State createState() => _ResourcesState(); -} - -class _ResourcesState extends State { - List externalProviders = []; - - List> providerItemKeys = []; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _syncExternalProviders(); - }); - } - - _syncExternalProviders() async { - externalProviders = await clashCore.getExternalProviders(); - if (mounted) { - setState(() {}); - } - } - - _updateProviders() async { - final updateProviders = providerItemKeys.map( - (key) async => await key.currentState?.updateProvider(false), - ); - await Future.wait(updateProviders); - _syncExternalProviders(); - } - - List _buildExternalProviderSection() { - List> keys = []; - final res = generateInfoSection( - info: Info( - iconData: Icons.source, - label: appLocalizations.externalResources, - ), - actions: [ - IconButton.filledTonal( - onPressed: () { - _updateProviders(); - }, - padding: const EdgeInsets.all(4), - iconSize: 20, - icon: const Icon( - Icons.sync, - ), - ) - ], - items: externalProviders.map( - (externalProvider) { - final key = - GlobalObjectKey<_ProviderItemState>(externalProvider.name); - keys.add(key); - return ProviderItem( - key: key, - provider: externalProvider, - onUpdated: () { - _syncExternalProviders(); - }, - ); - }, - ), - ); - providerItemKeys = keys; - return res; - } - - List _buildGeoDataSection() { + Widget build(BuildContext context) { const geoItems = [ GeoItem( label: "GeoIp", @@ -111,26 +42,19 @@ class _ResourcesState extends State { GeoItem(label: "ASN", fileName: asnFileName, key: "asn"), ]; - return generateInfoSection( - info: Info( - iconData: Icons.storage, - label: appLocalizations.geoData, - ), - items: geoItems.map( - (geoItem) => GeoDataListItem( + return ListView.separated( + itemBuilder: (_, index) { + final geoItem = geoItems[index]; + return GeoDataListItem( geoItem: geoItem, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return generateListView( - [ - ..._buildGeoDataSection(), - ..._buildExternalProviderSection(), - ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 0, + ); + }, + itemCount: geoItems.length, ); } } @@ -226,9 +150,6 @@ class _GeoDataListItemState extends State { const SizedBox( height: 8, ), - const SizedBox( - height: 8, - ), Wrap( runSpacing: 6, spacing: 12, @@ -315,117 +236,6 @@ class _GeoDataListItemState extends State { } } -class ProviderItem extends StatefulWidget { - final ExternalProvider provider; - final Function onUpdated; - - const ProviderItem({ - super.key, - required this.provider, - required this.onUpdated, - }); - - @override - State createState() => _ProviderItemState(); -} - -class _ProviderItemState extends State { - final isUpdating = ValueNotifier(false); - - ExternalProvider get provider => widget.provider; - - _handleUpdateProfile() async { - await globalState.safeRun(updateProvider); - widget.onUpdated(); - } - - updateProvider([isSingle = true]) async { - if (provider.vehicleType != "HTTP") return; - isUpdating.value = true; - try { - final message = await clashCore.updateExternalProvider( - providerName: provider.name, - providerType: provider.type, - ); - if (message.isNotEmpty) throw message; - } catch (e) { - isUpdating.value = false; - if (!isSingle) { - return e.toString(); - } else { - rethrow; - } - } - isUpdating.value = false; - return null; - } - - String _buildProviderDesc() { - return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}"; - } - - @override - void dispose() { - super.dispose(); - isUpdating.dispose(); - } - - Widget _buildSubtitle() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4, - ), - Text( - _buildProviderDesc(), - ), - if (provider.vehicleType == "HTTP") ...[ - const SizedBox( - height: 8, - ), - CommonChip( - avatar: const Icon(Icons.sync), - label: appLocalizations.sync, - onPressed: () { - _handleUpdateProfile(); - }, - ), - ], - ], - ); - } - - @override - Widget build(BuildContext context) { - return ListItem( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - title: Text(provider.name), - subtitle: _buildSubtitle(), - trailing: SizedBox( - height: 48, - width: 48, - child: ValueListenableBuilder( - valueListenable: isUpdating, - builder: (_, isUpdating, ___) { - return FadeBox( - child: isUpdating - ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : const SizedBox(), - ); - }, - ), - ), - ); - } -} - class UpdateGeoUrlFormDialog extends StatefulWidget { final String title; final String url; diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 7a59f20..43156a6 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -66,6 +66,7 @@ "hours": "Hours", "days": "Days", "minutes": "Minutes", + "seconds": "Seconds", "ago": " Ago", "just": "Just", "qrcode": "QR code", @@ -130,12 +131,10 @@ "notSelectedTip": "The current proxy group cannot be selected.", "tip": "tip", "backupAndRecovery": "Backup and Recovery", - "backupAndRecoveryDesc": "Sync data by WebDAV", + "backupAndRecoveryDesc": "Sync data via WebDAV or file", "account": "Account", "backup": "Backup", - "backupDesc": "Backup local data to WebDAV", "recovery": "Recovery", - "recoveryDesc": "Recovery data from WebDAV", "recoveryProfiles": "Only recovery profiles", "recoveryAll": "Recovery all data", "recoverySuccess": "Recovery success", @@ -220,5 +219,13 @@ "onlyStatisticsProxy": "Only statistics proxy", "onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic", "deleteProfileTip": "Sure you want to delete the current profile?", - "prueBlackMode": "Prue black mode" + "prueBlackMode": "Prue black mode", + "keepAliveIntervalDesc": "Tcp keep alive interval", + "entries": " entries", + "local": "Local", + "remote": "Remote", + "remoteBackupDesc": "Backup local data to WebDAV", + "remoteRecoveryDesc": "Recovery data from WebDAV", + "localBackupDesc": "Backup local data to local", + "localRecoveryDesc": "Recovery data from file" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index f8e6009..7a925b6 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -66,6 +66,7 @@ "hours": "小时", "days": "天", "minutes": "分钟", + "seconds": "秒", "ago": "前", "just": "刚刚", "qrcode": "二维码", @@ -130,12 +131,10 @@ "notSelectedTip": "当前代理组无法选中", "tip": "提示", "backupAndRecovery": "备份与恢复", - "backupAndRecoveryDesc": "通过WebDAV同步数据", + "backupAndRecoveryDesc": "通过WebDAV或者文件同步数据", "account": "账号", "backup": "备份", - "backupDesc": "备份数据到WebDAV", "recovery": "恢复", - "recoveryDesc": "从WebDAV恢复数据", "recoveryProfiles": "仅恢复配置文件", "recoveryAll": "恢复所有数据", "recoverySuccess": "恢复成功", @@ -220,5 +219,13 @@ "onlyStatisticsProxy": "仅统计代理", "onlyStatisticsProxyDesc": "开启后,将只统计代理流量", "deleteProfileTip": "确定要删除当前配置吗?", - "prueBlackMode": "纯黑模式" + "prueBlackMode": "纯黑模式", + "keepAliveIntervalDesc": "TCP保持活动间隔", + "entries": "个条目", + "local": "本地", + "remote": "远程", + "remoteBackupDesc": "备份数据到WebDAV", + "remoteRecoveryDesc": "通过WebDAV恢复数据", + "localBackupDesc": "备份数据到本地", + "localRecoveryDesc": "通过文件恢复数据" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index a5f56b7..885c395 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -74,10 +74,8 @@ class MessageLookup extends MessageLookupByLibrary { "backup": MessageLookupByLibrary.simpleMessage("Backup"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("Backup and Recovery"), - "backupAndRecoveryDesc": - MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"), - "backupDesc": - MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"), + "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( + "Sync data via WebDAV or file"), "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"), "bind": MessageLookupByLibrary.simpleMessage("Bind"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), @@ -129,6 +127,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("Download"), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "en": MessageLookupByLibrary.simpleMessage("English"), + "entries": MessageLookupByLibrary.simpleMessage(" entries"), "exclude": MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"), "excludeDesc": MessageLookupByLibrary.simpleMessage( @@ -172,9 +171,16 @@ class MessageLookup extends MessageLookupByLibrary { "ipv6Desc": MessageLookupByLibrary.simpleMessage( "When turned on it will be able to receive IPv6 traffic"), "just": MessageLookupByLibrary.simpleMessage("Just"), + "keepAliveIntervalDesc": + MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"), "language": MessageLookupByLibrary.simpleMessage("Language"), "light": MessageLookupByLibrary.simpleMessage("Light"), "list": MessageLookupByLibrary.simpleMessage("List"), + "local": MessageLookupByLibrary.simpleMessage("Local"), + "localBackupDesc": + MessageLookupByLibrary.simpleMessage("Backup local data to local"), + "localRecoveryDesc": + MessageLookupByLibrary.simpleMessage("Recovery data from file"), "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), "logcat": MessageLookupByLibrary.simpleMessage("Logcat"), "logcatDesc": MessageLookupByLibrary.simpleMessage( @@ -265,12 +271,15 @@ class MessageLookup extends MessageLookupByLibrary { "recovery": MessageLookupByLibrary.simpleMessage("Recovery"), "recoveryAll": MessageLookupByLibrary.simpleMessage("Recovery all data"), - "recoveryDesc": - MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("Only recovery profiles"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"), + "remote": MessageLookupByLibrary.simpleMessage("Remote"), + "remoteBackupDesc": + MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"), + "remoteRecoveryDesc": + MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"), "requests": MessageLookupByLibrary.simpleMessage("Requests"), "requestsDesc": MessageLookupByLibrary.simpleMessage( "View recently request records"), @@ -280,6 +289,7 @@ class MessageLookup extends MessageLookupByLibrary { "rule": MessageLookupByLibrary.simpleMessage("Rule"), "save": MessageLookupByLibrary.simpleMessage("Save"), "search": MessageLookupByLibrary.simpleMessage("Search"), + "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selected": MessageLookupByLibrary.simpleMessage("Selected"), "settings": MessageLookupByLibrary.simpleMessage("Settings"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 6cb107a..99e2e58 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -62,8 +62,7 @@ class MessageLookup extends MessageLookupByLibrary { "backup": MessageLookupByLibrary.simpleMessage("备份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecoveryDesc": - MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"), - "backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), + MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "bind": MessageLookupByLibrary.simpleMessage("绑定"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), @@ -106,6 +105,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("下载"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "en": MessageLookupByLibrary.simpleMessage("英语"), + "entries": MessageLookupByLibrary.simpleMessage("个条目"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), @@ -139,9 +139,14 @@ class MessageLookup extends MessageLookupByLibrary { "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), + "keepAliveIntervalDesc": + MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "language": MessageLookupByLibrary.simpleMessage("语言"), "light": MessageLookupByLibrary.simpleMessage("浅色"), "list": MessageLookupByLibrary.simpleMessage("列表"), + "local": MessageLookupByLibrary.simpleMessage("本地"), + "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), + "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), @@ -213,9 +218,12 @@ class MessageLookup extends MessageLookupByLibrary { "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), - "recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), + "remote": MessageLookupByLibrary.simpleMessage("远程"), + "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), + "remoteRecoveryDesc": + MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"), "requests": MessageLookupByLibrary.simpleMessage("请求"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "resources": MessageLookupByLibrary.simpleMessage("资源"), @@ -223,6 +231,7 @@ class MessageLookup extends MessageLookupByLibrary { "rule": MessageLookupByLibrary.simpleMessage("规则"), "save": MessageLookupByLibrary.simpleMessage("保存"), "search": MessageLookupByLibrary.simpleMessage("搜索"), + "seconds": MessageLookupByLibrary.simpleMessage("秒"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), "settings": MessageLookupByLibrary.simpleMessage("设置"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index f4c99dc..796b6fa 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -720,6 +720,16 @@ class AppLocalizations { ); } + /// `Seconds` + String get seconds { + return Intl.message( + 'Seconds', + name: 'seconds', + desc: '', + args: [], + ); + } + /// ` Ago` String get ago { return Intl.message( @@ -1360,10 +1370,10 @@ class AppLocalizations { ); } - /// `Sync data by WebDAV` + /// `Sync data via WebDAV or file` String get backupAndRecoveryDesc { return Intl.message( - 'Sync data by WebDAV', + 'Sync data via WebDAV or file', name: 'backupAndRecoveryDesc', desc: '', args: [], @@ -1390,16 +1400,6 @@ class AppLocalizations { ); } - /// `Backup local data to WebDAV` - String get backupDesc { - return Intl.message( - 'Backup local data to WebDAV', - name: 'backupDesc', - desc: '', - args: [], - ); - } - /// `Recovery` String get recovery { return Intl.message( @@ -1410,16 +1410,6 @@ class AppLocalizations { ); } - /// `Recovery data from WebDAV` - String get recoveryDesc { - return Intl.message( - 'Recovery data from WebDAV', - name: 'recoveryDesc', - desc: '', - args: [], - ); - } - /// `Only recovery profiles` String get recoveryProfiles { return Intl.message( @@ -2269,6 +2259,86 @@ class AppLocalizations { args: [], ); } + + /// `Tcp keep alive interval` + String get keepAliveIntervalDesc { + return Intl.message( + 'Tcp keep alive interval', + name: 'keepAliveIntervalDesc', + desc: '', + args: [], + ); + } + + /// ` entries` + String get entries { + return Intl.message( + ' entries', + name: 'entries', + desc: '', + args: [], + ); + } + + /// `Local` + String get local { + return Intl.message( + 'Local', + name: 'local', + desc: '', + args: [], + ); + } + + /// `Remote` + String get remote { + return Intl.message( + 'Remote', + name: 'remote', + desc: '', + args: [], + ); + } + + /// `Backup local data to WebDAV` + String get remoteBackupDesc { + return Intl.message( + 'Backup local data to WebDAV', + name: 'remoteBackupDesc', + desc: '', + args: [], + ); + } + + /// `Recovery data from WebDAV` + String get remoteRecoveryDesc { + return Intl.message( + 'Recovery data from WebDAV', + name: 'remoteRecoveryDesc', + desc: '', + args: [], + ); + } + + /// `Backup local data to local` + String get localBackupDesc { + return Intl.message( + 'Backup local data to local', + name: 'localBackupDesc', + desc: '', + args: [], + ); + } + + /// `Recovery data from file` + String get localRecoveryDesc { + return Intl.message( + 'Recovery data from file', + name: 'localRecoveryDesc', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/app.dart b/lib/models/app.dart index 081f649..071ebd3 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -8,6 +8,7 @@ import 'connection.dart'; import 'ffi.dart'; import 'log.dart'; import 'navigation.dart'; +import 'package.dart'; import 'profile.dart'; import 'proxy.dart'; import 'system_color_scheme.dart'; @@ -35,6 +36,8 @@ class AppState with ChangeNotifier { double _viewWidth; List _requests; num _checkIpNum; + List _providers; + List _packages; AppState({ required Mode mode, @@ -54,6 +57,8 @@ class AppState with ChangeNotifier { _totalTraffic = Traffic(), _delayMap = {}, _groups = [], + _providers = [], + _packages = [], _isCompatible = isCompatible, _systemColorSchemes = const SystemColorSchemes(); @@ -330,6 +335,31 @@ class AppState with ChangeNotifier { } } + List get packages => _packages; + + set packages(List value) { + if (!const ListEquality().equals(_packages, value)) { + _packages = value; + notifyListeners(); + } + } + + List get providers => _providers; + + set providers(List value) { + if (!const ListEquality().equals(_providers, value)) { + _providers = value; + notifyListeners(); + } + } + + setProvider(ExternalProvider provider) { + final index = _providers.indexWhere((item) => item.name == provider.name); + if (index == -1) return; + _providers = List.from(_providers)..[index] = provider; + notifyListeners(); + } + Group? getGroupWithName(String groupName) { final index = currentGroups.indexWhere((element) => element.name == groupName); diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 4d1b3bc..6aa4cfc 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -119,6 +119,7 @@ class ClashConfig extends ChangeNotifier { String _externalController; Mode _mode; FindProcessMode _findProcessMode; + int _keepAliveInterval; bool _unifiedDelay; bool _tcpConcurrent; Tun _tun; @@ -139,6 +140,7 @@ class ClashConfig extends ChangeNotifier { _unifiedDelay = false, _geodataLoader = geodataLoaderMemconservative, _externalController = '', + _keepAliveInterval = 30, _dns = Dns(), _geoXUrl = defaultGeoXMap, _rules = []; @@ -203,6 +205,16 @@ class ClashConfig extends ChangeNotifier { } } + @JsonKey(name: "keep-alive-interval", defaultValue: 30) + int get keepAliveInterval => _keepAliveInterval; + + set keepAliveInterval(int value) { + if (_keepAliveInterval != value) { + _keepAliveInterval = value; + notifyListeners(); + } + } + @JsonKey(defaultValue: false) bool get ipv6 => _ipv6; diff --git a/lib/models/config.dart b/lib/models/config.dart index 5384b67..11147fe 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -29,6 +29,7 @@ class AccessControl with _$AccessControl { class CoreState with _$CoreState { const factory CoreState({ AccessControl? accessControl, + required String currentProfileName, required bool allowBypass, required bool systemProxy, required int mixedPort, diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart index bd0bcb9..32510f0 100644 --- a/lib/models/ffi.dart +++ b/lib/models/ffi.dart @@ -24,7 +24,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams { @freezed class UpdateConfigParams with _$UpdateConfigParams { const factory UpdateConfigParams({ - @JsonKey(name: "profile-path") String? profilePath, + @JsonKey(name: "profile-id") required String profileId, required ClashConfig config, required ConfigExtendedParams params, }) = _UpdateConfigParams; @@ -123,6 +123,9 @@ class ExternalProvider with _$ExternalProvider { const factory ExternalProvider({ required String name, required String type, + required String path, + required int count, + @Default(false) bool isUpdating, @JsonKey(name: "vehicle-type") required String vehicleType, @JsonKey(name: "update-at") required DateTime updateAt, }) = _ExternalProvider; @@ -140,7 +143,7 @@ abstract mixin class AppMessageListener { void onStarted(String runTime) {} - void onLoaded(String groupName) {} + void onLoaded(String providerName) {} } abstract mixin class ServiceMessageListener { @@ -150,7 +153,5 @@ abstract mixin class ServiceMessageListener { onStarted(String runTime) {} - onLoaded(String groupName) {} + onLoaded(String providerName) {} } - - diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 006a934..5e4c183 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -45,6 +45,7 @@ ClashConfig _$ClashConfigFromJson(Map json) => ClashConfig() ..logLevel = $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info ..externalController = json['external-controller'] as String? ?? '' + ..keepAliveInterval = (json['keep-alive-interval'] as num?)?.toInt() ?? 30 ..ipv6 = json['ipv6'] as bool? ?? false ..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative' ..unifiedDelay = json['unified-delay'] as bool? ?? false @@ -75,6 +76,7 @@ Map _$ClashConfigToJson(ClashConfig instance) => 'allow-lan': instance.allowLan, 'log-level': _$LogLevelEnumMap[instance.logLevel]!, 'external-controller': instance.externalController, + 'keep-alive-interval': instance.keepAliveInterval, 'ipv6': instance.ipv6, 'geodata-loader': instance.geodataLoader, 'unified-delay': instance.unifiedDelay, diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index b708a27..18e0567 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -247,6 +247,7 @@ CoreState _$CoreStateFromJson(Map json) { /// @nodoc mixin _$CoreState { AccessControl? get accessControl => throw _privateConstructorUsedError; + String get currentProfileName => throw _privateConstructorUsedError; bool get allowBypass => throw _privateConstructorUsedError; bool get systemProxy => throw _privateConstructorUsedError; int get mixedPort => throw _privateConstructorUsedError; @@ -265,6 +266,7 @@ abstract class $CoreStateCopyWith<$Res> { @useResult $Res call( {AccessControl? accessControl, + String currentProfileName, bool allowBypass, bool systemProxy, int mixedPort, @@ -287,6 +289,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> @override $Res call({ Object? accessControl = freezed, + Object? currentProfileName = null, Object? allowBypass = null, Object? systemProxy = null, Object? mixedPort = null, @@ -297,6 +300,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable as AccessControl?, + currentProfileName: null == currentProfileName + ? _value.currentProfileName + : currentProfileName // ignore: cast_nullable_to_non_nullable + as String, allowBypass: null == allowBypass ? _value.allowBypass : allowBypass // ignore: cast_nullable_to_non_nullable @@ -339,6 +346,7 @@ abstract class _$$CoreStateImplCopyWith<$Res> @useResult $Res call( {AccessControl? accessControl, + String currentProfileName, bool allowBypass, bool systemProxy, int mixedPort, @@ -360,6 +368,7 @@ class __$$CoreStateImplCopyWithImpl<$Res> @override $Res call({ Object? accessControl = freezed, + Object? currentProfileName = null, Object? allowBypass = null, Object? systemProxy = null, Object? mixedPort = null, @@ -370,6 +379,10 @@ class __$$CoreStateImplCopyWithImpl<$Res> ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable as AccessControl?, + currentProfileName: null == currentProfileName + ? _value.currentProfileName + : currentProfileName // ignore: cast_nullable_to_non_nullable + as String, allowBypass: null == allowBypass ? _value.allowBypass : allowBypass // ignore: cast_nullable_to_non_nullable @@ -395,6 +408,7 @@ class __$$CoreStateImplCopyWithImpl<$Res> class _$CoreStateImpl implements _CoreState { const _$CoreStateImpl( {this.accessControl, + required this.currentProfileName, required this.allowBypass, required this.systemProxy, required this.mixedPort, @@ -406,6 +420,8 @@ class _$CoreStateImpl implements _CoreState { @override final AccessControl? accessControl; @override + final String currentProfileName; + @override final bool allowBypass; @override final bool systemProxy; @@ -416,7 +432,7 @@ class _$CoreStateImpl implements _CoreState { @override String toString() { - return 'CoreState(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; + return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; } @override @@ -426,6 +442,8 @@ class _$CoreStateImpl implements _CoreState { other is _$CoreStateImpl && (identical(other.accessControl, accessControl) || other.accessControl == accessControl) && + (identical(other.currentProfileName, currentProfileName) || + other.currentProfileName == currentProfileName) && (identical(other.allowBypass, allowBypass) || other.allowBypass == allowBypass) && (identical(other.systemProxy, systemProxy) || @@ -438,8 +456,8 @@ class _$CoreStateImpl implements _CoreState { @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, accessControl, allowBypass, - systemProxy, mixedPort, onlyProxy); + int get hashCode => Object.hash(runtimeType, accessControl, + currentProfileName, allowBypass, systemProxy, mixedPort, onlyProxy); @JsonKey(ignore: true) @override @@ -458,6 +476,7 @@ class _$CoreStateImpl implements _CoreState { abstract class _CoreState implements CoreState { const factory _CoreState( {final AccessControl? accessControl, + required final String currentProfileName, required final bool allowBypass, required final bool systemProxy, required final int mixedPort, @@ -469,6 +488,8 @@ abstract class _CoreState implements CoreState { @override AccessControl? get accessControl; @override + String get currentProfileName; + @override bool get allowBypass; @override bool get systemProxy; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index db70958..dff1c14 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -139,6 +139,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => ? null : AccessControl.fromJson( json['accessControl'] as Map), + currentProfileName: json['currentProfileName'] as String, allowBypass: json['allowBypass'] as bool, systemProxy: json['systemProxy'] as bool, mixedPort: (json['mixedPort'] as num).toInt(), @@ -148,6 +149,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => Map _$$CoreStateImplToJson(_$CoreStateImpl instance) => { 'accessControl': instance.accessControl, + 'currentProfileName': instance.currentProfileName, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, 'mixedPort': instance.mixedPort, diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/ffi.freezed.dart index 346eb60..8174de3 100644 --- a/lib/models/generated/ffi.freezed.dart +++ b/lib/models/generated/ffi.freezed.dart @@ -248,8 +248,8 @@ UpdateConfigParams _$UpdateConfigParamsFromJson(Map json) { /// @nodoc mixin _$UpdateConfigParams { - @JsonKey(name: "profile-path") - String? get profilePath => throw _privateConstructorUsedError; + @JsonKey(name: "profile-id") + String get profileId => throw _privateConstructorUsedError; ClashConfig get config => throw _privateConstructorUsedError; ConfigExtendedParams get params => throw _privateConstructorUsedError; @@ -266,7 +266,7 @@ abstract class $UpdateConfigParamsCopyWith<$Res> { _$UpdateConfigParamsCopyWithImpl<$Res, UpdateConfigParams>; @useResult $Res call( - {@JsonKey(name: "profile-path") String? profilePath, + {@JsonKey(name: "profile-id") String profileId, ClashConfig config, ConfigExtendedParams params}); @@ -286,15 +286,15 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams> @pragma('vm:prefer-inline') @override $Res call({ - Object? profilePath = freezed, + Object? profileId = null, Object? config = null, Object? params = null, }) { return _then(_value.copyWith( - profilePath: freezed == profilePath - ? _value.profilePath - : profilePath // ignore: cast_nullable_to_non_nullable - as String?, + profileId: null == profileId + ? _value.profileId + : profileId // ignore: cast_nullable_to_non_nullable + as String, config: null == config ? _value.config : config // ignore: cast_nullable_to_non_nullable @@ -324,7 +324,7 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: "profile-path") String? profilePath, + {@JsonKey(name: "profile-id") String profileId, ClashConfig config, ConfigExtendedParams params}); @@ -343,15 +343,15 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? profilePath = freezed, + Object? profileId = null, Object? config = null, Object? params = null, }) { return _then(_$UpdateConfigParamsImpl( - profilePath: freezed == profilePath - ? _value.profilePath - : profilePath // ignore: cast_nullable_to_non_nullable - as String?, + profileId: null == profileId + ? _value.profileId + : profileId // ignore: cast_nullable_to_non_nullable + as String, config: null == config ? _value.config : config // ignore: cast_nullable_to_non_nullable @@ -368,7 +368,7 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res> @JsonSerializable() class _$UpdateConfigParamsImpl implements _UpdateConfigParams { const _$UpdateConfigParamsImpl( - {@JsonKey(name: "profile-path") this.profilePath, + {@JsonKey(name: "profile-id") required this.profileId, required this.config, required this.params}); @@ -376,8 +376,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { _$$UpdateConfigParamsImplFromJson(json); @override - @JsonKey(name: "profile-path") - final String? profilePath; + @JsonKey(name: "profile-id") + final String profileId; @override final ClashConfig config; @override @@ -385,7 +385,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { @override String toString() { - return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)'; + return 'UpdateConfigParams(profileId: $profileId, config: $config, params: $params)'; } @override @@ -393,15 +393,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UpdateConfigParamsImpl && - (identical(other.profilePath, profilePath) || - other.profilePath == profilePath) && + (identical(other.profileId, profileId) || + other.profileId == profileId) && (identical(other.config, config) || other.config == config) && (identical(other.params, params) || other.params == params)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, profilePath, config, params); + int get hashCode => Object.hash(runtimeType, profileId, config, params); @JsonKey(ignore: true) @override @@ -420,7 +420,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { abstract class _UpdateConfigParams implements UpdateConfigParams { const factory _UpdateConfigParams( - {@JsonKey(name: "profile-path") final String? profilePath, + {@JsonKey(name: "profile-id") required final String profileId, required final ClashConfig config, required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl; @@ -428,8 +428,8 @@ abstract class _UpdateConfigParams implements UpdateConfigParams { _$UpdateConfigParamsImpl.fromJson; @override - @JsonKey(name: "profile-path") - String? get profilePath; + @JsonKey(name: "profile-id") + String get profileId; @override ClashConfig get config; @override @@ -1687,6 +1687,9 @@ ExternalProvider _$ExternalProviderFromJson(Map json) { mixin _$ExternalProvider { String get name => throw _privateConstructorUsedError; String get type => throw _privateConstructorUsedError; + String get path => throw _privateConstructorUsedError; + int get count => throw _privateConstructorUsedError; + bool get isUpdating => throw _privateConstructorUsedError; @JsonKey(name: "vehicle-type") String get vehicleType => throw _privateConstructorUsedError; @JsonKey(name: "update-at") @@ -1707,6 +1710,9 @@ abstract class $ExternalProviderCopyWith<$Res> { $Res call( {String name, String type, + String path, + int count, + bool isUpdating, @JsonKey(name: "vehicle-type") String vehicleType, @JsonKey(name: "update-at") DateTime updateAt}); } @@ -1726,6 +1732,9 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider> $Res call({ Object? name = null, Object? type = null, + Object? path = null, + Object? count = null, + Object? isUpdating = null, Object? vehicleType = null, Object? updateAt = null, }) { @@ -1738,6 +1747,18 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider> ? _value.type : type // ignore: cast_nullable_to_non_nullable as String, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + count: null == count + ? _value.count + : count // ignore: cast_nullable_to_non_nullable + as int, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, vehicleType: null == vehicleType ? _value.vehicleType : vehicleType // ignore: cast_nullable_to_non_nullable @@ -1761,6 +1782,9 @@ abstract class _$$ExternalProviderImplCopyWith<$Res> $Res call( {String name, String type, + String path, + int count, + bool isUpdating, @JsonKey(name: "vehicle-type") String vehicleType, @JsonKey(name: "update-at") DateTime updateAt}); } @@ -1778,6 +1802,9 @@ class __$$ExternalProviderImplCopyWithImpl<$Res> $Res call({ Object? name = null, Object? type = null, + Object? path = null, + Object? count = null, + Object? isUpdating = null, Object? vehicleType = null, Object? updateAt = null, }) { @@ -1790,6 +1817,18 @@ class __$$ExternalProviderImplCopyWithImpl<$Res> ? _value.type : type // ignore: cast_nullable_to_non_nullable as String, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + count: null == count + ? _value.count + : count // ignore: cast_nullable_to_non_nullable + as int, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, vehicleType: null == vehicleType ? _value.vehicleType : vehicleType // ignore: cast_nullable_to_non_nullable @@ -1808,6 +1847,9 @@ class _$ExternalProviderImpl implements _ExternalProvider { const _$ExternalProviderImpl( {required this.name, required this.type, + required this.path, + required this.count, + this.isUpdating = false, @JsonKey(name: "vehicle-type") required this.vehicleType, @JsonKey(name: "update-at") required this.updateAt}); @@ -1819,6 +1861,13 @@ class _$ExternalProviderImpl implements _ExternalProvider { @override final String type; @override + final String path; + @override + final int count; + @override + @JsonKey() + final bool isUpdating; + @override @JsonKey(name: "vehicle-type") final String vehicleType; @override @@ -1827,7 +1876,7 @@ class _$ExternalProviderImpl implements _ExternalProvider { @override String toString() { - return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)'; + return 'ExternalProvider(name: $name, type: $type, path: $path, count: $count, isUpdating: $isUpdating, vehicleType: $vehicleType, updateAt: $updateAt)'; } @override @@ -1837,6 +1886,10 @@ class _$ExternalProviderImpl implements _ExternalProvider { other is _$ExternalProviderImpl && (identical(other.name, name) || other.name == name) && (identical(other.type, type) || other.type == type) && + (identical(other.path, path) || other.path == path) && + (identical(other.count, count) || other.count == count) && + (identical(other.isUpdating, isUpdating) || + other.isUpdating == isUpdating) && (identical(other.vehicleType, vehicleType) || other.vehicleType == vehicleType) && (identical(other.updateAt, updateAt) || @@ -1845,8 +1898,8 @@ class _$ExternalProviderImpl implements _ExternalProvider { @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, name, type, vehicleType, updateAt); + int get hashCode => Object.hash( + runtimeType, name, type, path, count, isUpdating, vehicleType, updateAt); @JsonKey(ignore: true) @override @@ -1867,6 +1920,9 @@ abstract class _ExternalProvider implements ExternalProvider { const factory _ExternalProvider( {required final String name, required final String type, + required final String path, + required final int count, + final bool isUpdating, @JsonKey(name: "vehicle-type") required final String vehicleType, @JsonKey(name: "update-at") required final DateTime updateAt}) = _$ExternalProviderImpl; @@ -1879,6 +1935,12 @@ abstract class _ExternalProvider implements ExternalProvider { @override String get type; @override + String get path; + @override + int get count; + @override + bool get isUpdating; + @override @JsonKey(name: "vehicle-type") String get vehicleType; @override diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/ffi.g.dart index ee6d372..42670ec 100644 --- a/lib/models/generated/ffi.g.dart +++ b/lib/models/generated/ffi.g.dart @@ -27,7 +27,7 @@ Map _$$ConfigExtendedParamsImplToJson( _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( Map json) => _$UpdateConfigParamsImpl( - profilePath: json['profile-path'] as String?, + profileId: json['profile-id'] as String, config: ClashConfig.fromJson(json['config'] as Map), params: ConfigExtendedParams.fromJson(json['params'] as Map), @@ -36,7 +36,7 @@ _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( Map _$$UpdateConfigParamsImplToJson( _$UpdateConfigParamsImpl instance) => { - 'profile-path': instance.profilePath, + 'profile-id': instance.profileId, 'config': instance.config, 'params': instance.params, }; @@ -156,6 +156,9 @@ _$ExternalProviderImpl _$$ExternalProviderImplFromJson( _$ExternalProviderImpl( name: json['name'] as String, type: json['type'] as String, + path: json['path'] as String, + count: (json['count'] as num).toInt(), + isUpdating: json['isUpdating'] as bool? ?? false, vehicleType: json['vehicle-type'] as String, updateAt: DateTime.parse(json['update-at'] as String), ); @@ -165,6 +168,9 @@ Map _$$ExternalProviderImplToJson( { 'name': instance.name, 'type': instance.type, + 'path': instance.path, + 'count': instance.count, + 'isUpdating': instance.isUpdating, 'vehicle-type': instance.vehicleType, 'update-at': instance.updateAt.toIso8601String(), }; diff --git a/lib/models/generated/profile.freezed.dart b/lib/models/generated/profile.freezed.dart index 7d45b1b..7263cc0 100644 --- a/lib/models/generated/profile.freezed.dart +++ b/lib/models/generated/profile.freezed.dart @@ -223,6 +223,8 @@ mixin _$Profile { bool get autoUpdate => throw _privateConstructorUsedError; Map get selectedMap => throw _privateConstructorUsedError; Set get unfoldSet => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false, includeFromJson: false) + bool get isUpdating => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -244,7 +246,8 @@ abstract class $ProfileCopyWith<$Res> { UserInfo? userInfo, bool autoUpdate, Map selectedMap, - Set unfoldSet}); + Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); $UserInfoCopyWith<$Res>? get userInfo; } @@ -272,6 +275,7 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? isUpdating = null, }) { return _then(_value.copyWith( id: null == id @@ -314,6 +318,10 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> ? _value.unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -347,7 +355,8 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> { UserInfo? userInfo, bool autoUpdate, Map selectedMap, - Set unfoldSet}); + Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); @override $UserInfoCopyWith<$Res>? get userInfo; @@ -374,6 +383,7 @@ class __$$ProfileImplCopyWithImpl<$Res> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? isUpdating = null, }) { return _then(_$ProfileImpl( id: null == id @@ -416,6 +426,10 @@ class __$$ProfileImplCopyWithImpl<$Res> ? _value._unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -433,7 +447,9 @@ class _$ProfileImpl implements _Profile { this.userInfo, this.autoUpdate = true, final Map selectedMap = const {}, - final Set unfoldSet = const {}}) + final Set unfoldSet = const {}, + @JsonKey(includeToJson: false, includeFromJson: false) + this.isUpdating = false}) : _selectedMap = selectedMap, _unfoldSet = unfoldSet; @@ -476,9 +492,13 @@ class _$ProfileImpl implements _Profile { return EqualUnmodifiableSetView(_unfoldSet); } + @override + @JsonKey(includeToJson: false, includeFromJson: false) + final bool isUpdating; + @override String toString() { - return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)'; + return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, isUpdating: $isUpdating)'; } @override @@ -502,7 +522,9 @@ class _$ProfileImpl implements _Profile { const DeepCollectionEquality() .equals(other._selectedMap, _selectedMap) && const DeepCollectionEquality() - .equals(other._unfoldSet, _unfoldSet)); + .equals(other._unfoldSet, _unfoldSet) && + (identical(other.isUpdating, isUpdating) || + other.isUpdating == isUpdating)); } @JsonKey(ignore: true) @@ -518,7 +540,8 @@ class _$ProfileImpl implements _Profile { userInfo, autoUpdate, const DeepCollectionEquality().hash(_selectedMap), - const DeepCollectionEquality().hash(_unfoldSet)); + const DeepCollectionEquality().hash(_unfoldSet), + isUpdating); @JsonKey(ignore: true) @override @@ -545,7 +568,9 @@ abstract class _Profile implements Profile { final UserInfo? userInfo, final bool autoUpdate, final Map selectedMap, - final Set unfoldSet}) = _$ProfileImpl; + final Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) + final bool isUpdating}) = _$ProfileImpl; factory _Profile.fromJson(Map json) = _$ProfileImpl.fromJson; @@ -570,6 +595,9 @@ abstract class _Profile implements Profile { @override Set get unfoldSet; @override + @JsonKey(includeToJson: false, includeFromJson: false) + bool get isUpdating; + @override @JsonKey(ignore: true) _$$ProfileImplCopyWith<_$ProfileImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 3e585d9..1405f1d 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -2815,32 +2815,28 @@ abstract class _ProxiesListHeaderSelectorState } /// @nodoc -mixin _$CurrentGroupProxyNameSelectorState { - String? get proxyName => throw _privateConstructorUsedError; - String? get proxyName2 => throw _privateConstructorUsedError; +mixin _$ProxiesActionsState { + bool get isCurrent => throw _privateConstructorUsedError; + bool get hasProvider => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $CurrentGroupProxyNameSelectorStateCopyWith< - CurrentGroupProxyNameSelectorState> - get copyWith => throw _privateConstructorUsedError; + $ProxiesActionsStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - factory $CurrentGroupProxyNameSelectorStateCopyWith( - CurrentGroupProxyNameSelectorState value, - $Res Function(CurrentGroupProxyNameSelectorState) then) = - _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - CurrentGroupProxyNameSelectorState>; +abstract class $ProxiesActionsStateCopyWith<$Res> { + factory $ProxiesActionsStateCopyWith( + ProxiesActionsState value, $Res Function(ProxiesActionsState) then) = + _$ProxiesActionsStateCopyWithImpl<$Res, ProxiesActionsState>; @useResult - $Res call({String? proxyName, String? proxyName2}); + $Res call({bool isCurrent, bool hasProvider}); } /// @nodoc -class _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - $Val extends CurrentGroupProxyNameSelectorState> - implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - _$CurrentGroupProxyNameSelectorStateCopyWithImpl(this._value, this._then); +class _$ProxiesActionsStateCopyWithImpl<$Res, $Val extends ProxiesActionsState> + implements $ProxiesActionsStateCopyWith<$Res> { + _$ProxiesActionsStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -2850,117 +2846,109 @@ class _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? proxyName = freezed, - Object? proxyName2 = freezed, + Object? isCurrent = null, + Object? hasProvider = null, }) { return _then(_value.copyWith( - proxyName: freezed == proxyName - ? _value.proxyName - : proxyName // ignore: cast_nullable_to_non_nullable - as String?, - proxyName2: freezed == proxyName2 - ? _value.proxyName2 - : proxyName2 // ignore: cast_nullable_to_non_nullable - as String?, + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + hasProvider: null == hasProvider + ? _value.hasProvider + : hasProvider // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } /// @nodoc -abstract class _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> - implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - factory _$$CurrentGroupProxyNameSelectorStateImplCopyWith( - _$CurrentGroupProxyNameSelectorStateImpl value, - $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) then) = - __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res>; +abstract class _$$ProxiesActionsStateImplCopyWith<$Res> + implements $ProxiesActionsStateCopyWith<$Res> { + factory _$$ProxiesActionsStateImplCopyWith(_$ProxiesActionsStateImpl value, + $Res Function(_$ProxiesActionsStateImpl) then) = + __$$ProxiesActionsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({String? proxyName, String? proxyName2}); + $Res call({bool isCurrent, bool hasProvider}); } /// @nodoc -class __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res> - extends _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - _$CurrentGroupProxyNameSelectorStateImpl> - implements _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> { - __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl( - _$CurrentGroupProxyNameSelectorStateImpl _value, - $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) _then) +class __$$ProxiesActionsStateImplCopyWithImpl<$Res> + extends _$ProxiesActionsStateCopyWithImpl<$Res, _$ProxiesActionsStateImpl> + implements _$$ProxiesActionsStateImplCopyWith<$Res> { + __$$ProxiesActionsStateImplCopyWithImpl(_$ProxiesActionsStateImpl _value, + $Res Function(_$ProxiesActionsStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? proxyName = freezed, - Object? proxyName2 = freezed, + Object? isCurrent = null, + Object? hasProvider = null, }) { - return _then(_$CurrentGroupProxyNameSelectorStateImpl( - proxyName: freezed == proxyName - ? _value.proxyName - : proxyName // ignore: cast_nullable_to_non_nullable - as String?, - proxyName2: freezed == proxyName2 - ? _value.proxyName2 - : proxyName2 // ignore: cast_nullable_to_non_nullable - as String?, + return _then(_$ProxiesActionsStateImpl( + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + hasProvider: null == hasProvider + ? _value.hasProvider + : hasProvider // ignore: cast_nullable_to_non_nullable + as bool, )); } } /// @nodoc -class _$CurrentGroupProxyNameSelectorStateImpl - implements _CurrentGroupProxyNameSelectorState { - const _$CurrentGroupProxyNameSelectorStateImpl( - {required this.proxyName, required this.proxyName2}); +class _$ProxiesActionsStateImpl implements _ProxiesActionsState { + const _$ProxiesActionsStateImpl( + {required this.isCurrent, required this.hasProvider}); @override - final String? proxyName; + final bool isCurrent; @override - final String? proxyName2; + final bool hasProvider; @override String toString() { - return 'CurrentGroupProxyNameSelectorState(proxyName: $proxyName, proxyName2: $proxyName2)'; + return 'ProxiesActionsState(isCurrent: $isCurrent, hasProvider: $hasProvider)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$CurrentGroupProxyNameSelectorStateImpl && - (identical(other.proxyName, proxyName) || - other.proxyName == proxyName) && - (identical(other.proxyName2, proxyName2) || - other.proxyName2 == proxyName2)); + other is _$ProxiesActionsStateImpl && + (identical(other.isCurrent, isCurrent) || + other.isCurrent == isCurrent) && + (identical(other.hasProvider, hasProvider) || + other.hasProvider == hasProvider)); } @override - int get hashCode => Object.hash(runtimeType, proxyName, proxyName2); + int get hashCode => Object.hash(runtimeType, isCurrent, hasProvider); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$CurrentGroupProxyNameSelectorStateImplCopyWith< - _$CurrentGroupProxyNameSelectorStateImpl> - get copyWith => __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl< - _$CurrentGroupProxyNameSelectorStateImpl>(this, _$identity); + _$$ProxiesActionsStateImplCopyWith<_$ProxiesActionsStateImpl> get copyWith => + __$$ProxiesActionsStateImplCopyWithImpl<_$ProxiesActionsStateImpl>( + this, _$identity); } -abstract class _CurrentGroupProxyNameSelectorState - implements CurrentGroupProxyNameSelectorState { - const factory _CurrentGroupProxyNameSelectorState( - {required final String? proxyName, - required final String? proxyName2}) = - _$CurrentGroupProxyNameSelectorStateImpl; +abstract class _ProxiesActionsState implements ProxiesActionsState { + const factory _ProxiesActionsState( + {required final bool isCurrent, + required final bool hasProvider}) = _$ProxiesActionsStateImpl; @override - String? get proxyName; + bool get isCurrent; @override - String? get proxyName2; + bool get hasProvider; @override @JsonKey(ignore: true) - _$$CurrentGroupProxyNameSelectorStateImplCopyWith< - _$CurrentGroupProxyNameSelectorStateImpl> - get copyWith => throw _privateConstructorUsedError; + _$$ProxiesActionsStateImplCopyWith<_$ProxiesActionsStateImpl> get copyWith => + throw _privateConstructorUsedError; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 1825f4c..b7439b8 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,3 +1,4 @@ +// ignore_for_file: invalid_annotation_target import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -55,6 +56,9 @@ class Profile with _$Profile { @Default(true) bool autoUpdate, @Default({}) SelectedMap selectedMap, @Default({}) Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) + @Default(false) + bool isUpdating, }) = _Profile; factory Profile.fromJson(Map json) => @@ -63,7 +67,7 @@ class Profile with _$Profile { factory Profile.normal({ String? label, String url = '', -}) { + }) { return Profile( label: label, url: url, @@ -77,8 +81,7 @@ extension ProfileExtension on Profile { ProfileType get type => url.isEmpty == true ? ProfileType.file : ProfileType.url; - bool get realAutoUpdate => - url.isEmpty == true ? false : autoUpdate; + bool get realAutoUpdate => url.isEmpty == true ? false : autoUpdate; Future checkAndUpdate() async { final isExists = await check(); diff --git a/lib/models/proxy.dart b/lib/models/proxy.dart index e122454..c3cb7cb 100644 --- a/lib/models/proxy.dart +++ b/lib/models/proxy.dart @@ -41,4 +41,4 @@ class Proxy with _$Proxy { }) = _Proxy; factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); -} +} \ No newline at end of file diff --git a/lib/models/selector.dart b/lib/models/selector.dart index addac12..0d18aab 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -137,7 +137,6 @@ class PackageListSelectorState with _$PackageListSelectorState { }) = _PackageListSelectorState; } - @freezed class ColumnsSelectorState with _$ColumnsSelectorState { const factory ColumnsSelectorState({ @@ -154,10 +153,11 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState { }) = _ProxiesListHeaderSelectorState; } + @freezed -class CurrentGroupProxyNameSelectorState with _$CurrentGroupProxyNameSelectorState { - const factory CurrentGroupProxyNameSelectorState({ - required String? proxyName, - required String? proxyName2, - }) = _CurrentGroupProxyNameSelectorState; +class ProxiesActionsState with _$ProxiesActionsState { + const factory ProxiesActionsState({ + required bool isCurrent, + required bool hasProvider, + }) = _ProxiesActionsState; } \ No newline at end of file diff --git a/lib/state.dart b/lib/state.dart index 6bf9fbc..351fc9f 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -45,11 +45,10 @@ class GlobalState { required Config config, bool isPatch = true, }) async { - final profilePath = await appPath.getProfilePath(config.currentProfileId); await config.currentProfile?.checkAndUpdate(); final res = await clashCore.updateConfig( UpdateConfigParams( - profilePath: profilePath, + profileId: config.currentProfileId ?? "", config: clashConfig, params: ConfigExtendedParams( isPatch: isPatch, @@ -96,8 +95,12 @@ class GlobalState { config: config, isPatch: false, ); - clashCore.setProfileName(config.currentProfile?.label ?? ''); await updateGroups(appState); + await updateProviders(appState); + } + + updateProviders(AppState appState) async { + appState.providers = await clashCore.getExternalProviders(); } init({ @@ -118,6 +121,7 @@ class GlobalState { systemProxy: config.systemProxy, mixedPort: clashConfig.mixedPort, onlyProxy: config.onlyProxy, + currentProfileName: config.currentProfile?.label ?? config.currentProfileId ?? "", ), ); } @@ -204,7 +208,7 @@ class GlobalState { final traffic = clashCore.getTraffic(); if (Platform.isAndroid && isVpnService == true) { proxy?.startForeground( - title: clashCore.getProfileName(), + title: clashCore.getState().currentProfileName, content: "$traffic", ); } else { diff --git a/lib/widgets/builder.dart b/lib/widgets/builder.dart index 829439d..7a1710f 100644 --- a/lib/widgets/builder.dart +++ b/lib/widgets/builder.dart @@ -1,4 +1,6 @@ +import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class ScrollOverBuilder extends StatefulWidget { final Widget Function(bool isOver) builder; @@ -15,7 +17,6 @@ class ScrollOverBuilder extends StatefulWidget { class _ScrollOverBuilderState extends State { final isOverNotifier = ValueNotifier(false); - @override void dispose() { super.dispose(); @@ -38,3 +39,29 @@ class _ScrollOverBuilderState extends State { ); } } + +class ProxiesActionsBuilder extends StatelessWidget { + final Widget? child; + final Widget Function( + ProxiesActionsState state, + Widget? child, + ) builder; + + const ProxiesActionsBuilder({ + super.key, + required this.child, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => ProxiesActionsState( + isCurrent: appState.currentLabel == "proxies", + hasProvider: appState.providers.isNotEmpty, + ), + builder: (_, state, child) => builder(state, child), + child: child, + ); + } +} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index a743251..1992064 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -32,40 +32,39 @@ class InfoHeader extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (info.iconData != null) ...[ - Icon( - info.iconData, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox( - width: 8, - ), - ], - Flexible( - child: TooltipText( - text: Text( - info.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - ], - ), Expanded( - flex: 1, child: Row( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, children: [ - ...actions, + if (info.iconData != null) ...[ + Icon( + info.iconData, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + width: 8, + ), + ], + Flexible( + child: TooltipText( + text: Text( + info.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), ], ), ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ...actions, + ], + ), ], ), ); @@ -146,13 +145,11 @@ class CommonCard extends StatelessWidget { childWidget = Column( mainAxisSize: MainAxisSize.min, children: [ - Flexible( - flex: 0, - child: InfoHeader( - info: info!, - ), + InfoHeader( + info: info!, ), Flexible( + flex: 1, child: child, ), ], diff --git a/lib/widgets/clash_container.dart b/lib/widgets/clash_container.dart index c3f7aa7..e38736b 100644 --- a/lib/widgets/clash_container.dart +++ b/lib/widgets/clash_container.dart @@ -27,6 +27,8 @@ class _ClashContainerState extends State systemProxy: config.systemProxy, mixedPort: clashConfig.mixedPort, onlyProxy: config.onlyProxy, + currentProfileName: + config.currentProfile?.label ?? config.currentProfileId ?? "", ), builder: (__, state, child) { clashCore.setState(state); @@ -36,9 +38,32 @@ class _ClashContainerState extends State ); } + _changeProfileHandle() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final appController = globalState.appController; + appController.appState.delayMap = {}; + await appController.applyProfile(); + }); + } + + Widget _changeProfileContainer(Widget child) { + return Selector( + selector: (_, config) => config.currentProfileId, + builder: (__, state, child) { + _changeProfileHandle(); + return child!; + }, + child: child, + ); + } + @override Widget build(BuildContext context) { - return _updateCoreState(widget.child); + return _changeProfileContainer( + _updateCoreState( + widget.child, + ), + ); } @override @@ -73,16 +98,15 @@ class _ClashContainerState extends State } @override - void onLoaded(String groupName) { + void onLoaded(String providerName) { final appController = globalState.appController; - final currentSelectedMap = appController.config.currentSelectedMap; - final proxyName = currentSelectedMap[groupName]; - if (proxyName == null) return; - appController.changeProxy( - groupName: groupName, - proxyName: proxyName, + appController.appState.setProvider( + clashCore.getExternalProvider( + providerName, + ), ); - super.onLoaded(proxyName); + appController.addCheckIpNumDebounce(); + super.onLoaded(providerName); } @override diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index d21b99e..2eccab3 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -5,10 +5,12 @@ import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; import 'side_sheet.dart'; -showExtendPage(BuildContext context, { +showExtendPage( + BuildContext context, { required Widget body, required String title, double? extendPageWidth, + bool forceNotSide = false, Widget? action, }) { final NavigatorState navigator = Navigator.of(context); @@ -17,23 +19,24 @@ showExtendPage(BuildContext context, { key: globalKey, child: body, ); - final isMobile = globalState.appController.appState.viewMode == - ViewMode.mobile; + final isMobile = + globalState.appController.appState.viewMode == ViewMode.mobile; + final isNotSide = isMobile || forceNotSide; navigator.push( ModalSideSheetRoute( modalBarrierColor: Colors.black38, builder: (context) { final commonScaffold = CommonScaffold( - automaticallyImplyLeading: isMobile ? true : false, - actions: isMobile + automaticallyImplyLeading: isNotSide, + actions: isNotSide ? null : [ - const SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ), - ], + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ), + ], title: title, body: uniqueBody, ); diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart index 09cdd11..008b01d 100644 --- a/lib/widgets/window_container.dart +++ b/lib/widgets/window_container.dart @@ -220,16 +220,14 @@ class _WindowHeaderState extends State { ), ), ), - if (!Platform.isMacOS) ...[ - const Positioned( - left: 0, - child: AppIcon(), - ), - Positioned( - right: 0, - child: _buildActions(), - ), - ] + const Positioned( + left: 0, + child: AppIcon(), + ), + Positioned( + right: 0, + child: _buildActions(), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 8e734a7..3775108 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,7 +34,7 @@ packages: source: hosted version: "3.5.1" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d @@ -525,6 +525,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + isolate_contactor: + dependency: transitive + description: + name: isolate_contactor + sha256: f1be0a90f91e4309ef37cc45280b2a84e769e848aae378318dd3dd263cfc482a + url: "https://pub.dev" + source: hosted + version: "4.2.0" + isolate_manager: + dependency: transitive + description: + name: isolate_manager + sha256: "8fb916c4444fd408f089448f904f083ac3e169ea1789fd4d987b25809af92188" + url: "https://pub.dev" + source: hosted + version: "4.3.1" jovial_misc: dependency: transitive description: @@ -828,6 +844,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + re_editor: + dependency: "direct main" + description: + name: re_editor + sha256: abae2b015799c936b9f9b68888e2c55007dd159b4654a85da22ce1af84efbd17 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + re_highlight: + dependency: "direct main" + description: + name: re_highlight + sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2" + url: "https://pub.dev" + source: hosted + version: "0.0.3" screen_retriever: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1cd0df3..7bcc912 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.50+202408011 +version: 0.8.51+202408051 environment: sdk: '>=3.1.0 <4.0.0' @@ -41,6 +41,9 @@ dependencies: win32: ^5.5.1 ffi: ^2.1.2 material_color_utilities: ^0.8.0 + re_editor: ^0.3.1 + re_highlight: ^0.0.3 + archive: ^3.6.1 dev_dependencies: flutter_test: sdk: flutter