Add compatibility mode and adapt clash scheme.

This commit is contained in:
chen08209
2024-05-10 10:11:27 +08:00
parent 08d07498b9
commit 472cea9037
31 changed files with 977 additions and 424 deletions

View File

@@ -3,6 +3,7 @@ package main
import "C"
import (
"github.com/metacubex/mihomo/adapter/inbound"
ap "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/component/resolver"
@@ -16,16 +17,53 @@ import (
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
)
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 GenerateConfigParams struct {
ProfilePath *string `json:"profile-path"`
Config *config.RawConfig `json:"config" `
IsPatch *bool `json:"is-patch"`
ProfilePath *string `json:"profile-path"`
Config *config.RawConfig `json:"config" `
IsPatch *bool `json:"is-patch"`
IsCompatible *bool `json:"is-compatible"`
}
type ChangeProxyParams struct {
@@ -91,6 +129,19 @@ func readFile(path string) ([]byte, error) {
return data, err
}
func removeFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
err = os.Remove(absPath)
if err != nil {
return err
}
return nil
}
func getRawConfigWithPath(path *string) *config.RawConfig {
if path == nil {
return config.DefaultRawConfig()
@@ -109,9 +160,9 @@ func getRawConfigWithPath(path *string) *config.RawConfig {
}
}
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
func decorationConfig(profilePath *string, cfg config.RawConfig, compatible bool) *config.RawConfig {
prof := getRawConfigWithPath(profilePath)
overwriteConfig(prof, cfg)
overwriteConfig(prof, cfg, compatible)
return prof
}
@@ -261,12 +312,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
*rule = computedRule
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
targetConfig.ExternalController = ""
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
targetConfig.IPv6 = patchConfig.IPv6
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.FindProcessMode = process.FindProcessAlways
targetConfig.AllowLan = patchConfig.AllowLan
@@ -286,9 +337,12 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
} else if runtime.GOOS == "windows" {
targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
}
targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
if compatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
}
}
func patchConfig(general *config.General) {

View File

@@ -8,6 +8,7 @@ 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/config"
"github.com/metacubex/mihomo/constant"
@@ -74,7 +75,7 @@ func updateConfig(s *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error())
return
}
prof := decorationConfig(params.ProfilePath, *params.Config)
prof := decorationConfig(params.ProfilePath, *params.Config, *params.IsCompatible)
currentConfig = prof
if *params.IsPatch {
applyConfig(true)
@@ -85,6 +86,39 @@ func updateConfig(s *C.char, port C.longlong) {
}()
}
//export clearEffect
func clearEffect(s *C.char) {
path := 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)
}()
}
//export getProxies
func getProxies() *C.char {
data, err := json.Marshal(tunnel.ProxiesWithProviders())
@@ -137,8 +171,7 @@ func getTraffic() *C.char {
}
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
func asyncTestDelay(s *C.char) {
go func() {
paramsString := C.GoString(s)
var params = &TestDelayParams{}
@@ -146,34 +179,42 @@ func asyncTestDelay(s *C.char, port C.longlong) {
if err != nil {
return
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
defer cancel()
proxies := tunnel.ProxiesWithProviders()
proxy := proxies[params.ProxyName]
delayData := &Delay{
Name: params.ProxyName,
}
message := bridge.Message{
Type: bridge.Delay,
Data: delayData,
}
if proxy == nil {
delayData.Value = -1
bridge.SendToPort(i, message.Json())
bridge.SendMessage(message)
return
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
bridge.SendToPort(i, message.Json())
bridge.SendMessage(message)
return
}
delayData.Value = int32(delay)
bridge.SendToPort(i, message.Json())
bridge.SendMessage(message)
}()
}

View File

@@ -31,7 +31,8 @@ runAppWithPreferences(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.currentProxyName = config.currentProxyName;
appState?.isCompatible = config.isCompatible;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
@@ -75,6 +76,7 @@ class ApplicationState extends State<Application> {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
appController.afterInit();
appController.initLink();
_updateGroups();
});
}
@@ -107,6 +109,18 @@ class ApplicationState extends State<Application> {
});
}
_updateGroups() {
if (globalState.groupsUpdateTimer != null) {
globalState.groupsUpdateTimer?.cancel();
globalState.groupsUpdateTimer = null;
}
globalState.groupsUpdateTimer ??=
Timer.periodic(appConstant.httpTimeoutDuration, (timer) async {
await appController.updateGroups();
appController.appState.sortNum++;
});
}
@override
Widget build(context) {
return AppStateContainer(

View File

@@ -107,43 +107,47 @@ class ClashCore {
});
}
Future<DelayMap> getDelayMap() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
return Isolate.run<DelayMap>(() {
final proxies = json.decode(proxiesRawString) as Map<String, dynamic>;
return proxies.map<String, int?>(
(k, v) {
final history = v["history"] as List<dynamic>;
if (history.isEmpty) {
return MapEntry(
k,
null,
);
} else {
final delay = history.last["delay"];
return MapEntry(
k,
delay != 0 ? delay : -1,
);
}
},
);
});
}
bool changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1;
}
Future<Delay> delay(String proxyName) {
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
final m = Message.fromJson(json.decode(message));
final delay = Delay.fromJson(m.data);
completer.complete(delay);
receiver.close();
}
});
bool delay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": appConstant.httpTimeoutDuration.inMilliseconds,
};
clashFFI.asyncTestDelay(
json.encode(delayParams).toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
Future.delayed(appConstant.httpTimeoutDuration + appConstant.moreDuration,
() {
if (!completer.isCompleted) {
receiver.close();
completer.complete(
Delay(
name: proxyName,
value: -1,
),
);
}
});
return completer.future;
clashFFI.asyncTestDelay(json.encode(delayParams).toNativeUtf8().cast());
return true;
}
clearEffect(String path) {
clashFFI.clearEffect(path.toNativeUtf8().cast());
}
healthcheck() {

View File

@@ -924,6 +924,20 @@ class ClashFFI {
late final _updateConfig =
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void clearEffect(
ffi.Pointer<ffi.Char> s,
) {
return _clearEffect(
s,
);
}
late final _clearEffectPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'clearEffect');
late final _clearEffect =
_clearEffectPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getProxies() {
return _getProxies();
}
@@ -960,20 +974,17 @@ class ClashFFI {
void asyncTestDelay(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _asyncTestDelay(
s,
port,
);
}
late final _asyncTestDelayPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
late final _asyncTestDelayPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'asyncTestDelay');
late final _asyncTestDelay =
_asyncTestDelayPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getVersionInfo() {
return _getVersionInfo();

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/services.dart';
import 'core.dart';
class ClashService {

View File

@@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -87,13 +85,7 @@ class AppController {
config.deleteProfileById(id);
final profilePath = await appPath.getProfilePath(id);
if (profilePath == null) return;
final file = File(profilePath);
Isolate.run(() async {
final isExists = await file.exists();
if (isExists) {
file.delete();
}
});
clashCore.clearEffect(profilePath);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
@@ -229,7 +221,6 @@ class AppController {
}
healthcheck() {
if (globalState.healthcheckLock) return;
for (final delay in appState.delayMap.entries) {
setDelay(
Delay(
@@ -245,6 +236,10 @@ class AppController {
appState.setDelay(delay);
}
updateDelayMap() async {
appState.delayMap = await clashCore.getDelayMap();
}
toPage(int index, {bool hasAnimate = false}) {
final nextLabel = globalState.currentNavigationItems[index].label;
appState.currentLabel = nextLabel;
@@ -355,4 +350,13 @@ class AppController {
},
);
}
clearShowProxyDelay() {
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
if (showProxyDelay != null) {
appState.setDelay(
Delay(name: showProxyDelay, value: null),
);
}
}
}

View File

@@ -35,6 +35,26 @@ class ApplicationSettingFragment extends StatelessWidget {
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: const Text("兼容模式"),
subtitle: const Text("开启将失去部分应用能力获得全量的Clash的支持"),
delegate: SwitchDelegate(
value: isCompatible,
onChanged: (bool value) async {
final appController = context.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.autoLaunch,

View File

@@ -120,7 +120,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
return ListView.separated(
itemBuilder: (_, index) {
return Container(
height: 84,
padding: kMaterialListPadding,
alignment: Alignment.center,
child: items[index],
);

View File

@@ -1,6 +1,6 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -25,11 +25,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
),
);
}
if (currentProxyName == UsedProxy.DIRECT.name) {
return const Icon(
Icons.offline_bolt_outlined,
);
}
if (delay == 0 || delay == null) {
return const AspectRatio(
aspectRatio: 1,
@@ -78,6 +73,52 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
}
_updateCurrentDelay(
String? currentProxyName,
int? delay,
bool isCurrent,
bool isInit,
) {
if (!isCurrent || currentProxyName == null || !isInit) return;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (delay == null) {
context.appController.setDelay(
Delay(
name: currentProxyName,
value: 0,
),
);
globalState.updateCurrentDelay(
currentProxyName,
);
}
});
}
_updateCurrentDelayContainer(Widget child) {
return Selector2<AppState, Config, UpdateCurrentDelaySelectorState>(
selector: (_, appState, config) {
return UpdateCurrentDelaySelectorState(
isInit: appState.isInit,
currentProxyName: appState.getRealProxyName(appState.showProxyName),
delay: appState
.delayMap[appState.getRealProxyName(appState.showProxyName)],
isCurrent: appState.currentLabel == 'dashboard',
);
},
builder: (_, state, __) {
_updateCurrentDelay(
state.currentProxyName,
state.delay,
state.isCurrent,
state.isInit,
);
return child;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
@@ -85,55 +126,57 @@ class _NetworkDetectionState extends State<NetworkDetection> {
iconData: Icons.network_check,
label: appLocalizations.networkDetection,
),
child: Selector3<AppState, Config, ClashConfig,
NetworkDetectionSelectorState>(
selector: (_, appState, config, clashConfig) {
final proxyName = appState.currentProxyName;
return NetworkDetectionSelectorState(
currentProxyName: proxyName,
delay: appState.getDelay(
proxyName,
),
);
},
builder: (_, state, __) {
return Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: TooltipText(
text: Text(
state.currentProxyName ?? appLocalizations.noProxy,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style:
Theme.of(context).textTheme.titleMedium?.toSoftBold(),
),
),
),
const SizedBox(
height: 8,
),
Flexible(
child: Container(
height: context.appController.measure.titleLargeHeight,
alignment: Alignment.centerLeft,
child: FadeBox(
child: _buildDescription(
state.currentProxyName,
state.delay,
child: _updateCurrentDelayContainer(
Selector<AppState, NetworkDetectionSelectorState>(
selector: (_, appState) {
return NetworkDetectionSelectorState(
currentProxyName: appState.showProxyName,
delay: appState.getDelay(
appState.showProxyName,
),
);
},
builder: (_, state, __) {
return Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: TooltipText(
text: Text(
state.currentProxyName ?? appLocalizations.noProxy,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleMedium
?.toSoftBold(),
),
),
),
),
],
),
);
},
const SizedBox(
height: 8,
),
Flexible(
child: Container(
height: context.appController.measure.titleLargeHeight,
alignment: Alignment.centerLeft,
child: FadeBox(
child: _buildDescription(
state.currentProxyName,
state.delay,
),
),
),
),
],
),
);
},
),
),
);
}

View File

@@ -11,10 +11,18 @@ class OutboundMode extends StatelessWidget {
_changeMode(BuildContext context, Mode? value) async {
final appController = context.appController;
final clashConfig = context.read<ClashConfig>();
final clashConfig = appController.clashConfig;
final config = appController.config;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
await appController.updateClashConfig();
if (!config.isCompatible) {
final proxySelected = config.currentSelectedMap[GroupName.Proxy.name];
final globalSelected = config.currentSelectedMap[GroupName.GLOBAL.name];
if (proxySelected != null && globalSelected == null) {
config.updateCurrentSelectedMap(GroupName.GLOBAL.name, proxySelected);
}
}
appController.changeProxy();
}
@@ -54,7 +62,8 @@ class OutboundMode extends StatelessWidget {
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
style: Theme
.of(context)
.textTheme
.titleMedium
?.toSoftBold(),

View File

@@ -1,7 +1,5 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -52,6 +50,9 @@ class _StartButtonState extends State<StartButton>
updateSystemProxy() async {
final appController = context.appController;
await appController.updateSystemProxy(isStart);
if (isStart && mounted) {
appController.clearShowProxyDelay();
}
}
@override

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:provider/provider.dart';
@@ -16,6 +17,8 @@ class ProxiesFragment extends StatefulWidget {
class _ProxiesFragmentState extends State<ProxiesFragment>
with TickerProviderStateMixin {
TabController? _tabController;
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
@@ -53,6 +56,87 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
});
}
@override
Widget build(BuildContext context) {
return DelayTestButtonContainer(
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const MaterialStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
)
],
);
},
),
),
);
}
}
class ProxiesTabView extends StatelessWidget {
final String groupName;
const ProxiesTabView({
super.key,
required this.groupName,
});
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
@@ -62,12 +146,11 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
List<Proxy> _sortOfDelay(BuildContext context, List<Proxy> proxies) {
final appState = context.read<AppState>();
final delayMap = appState.delayMap;
return proxies = List.of(proxies)
..sort(
(a, b) {
final aDelay = delayMap[a.name];
final bDelay = delayMap[b.name];
final aDelay = appState.delayMap[a.name];
final bDelay = appState.delayMap[b.name];
if (aDelay == null && bDelay == null) {
return 0;
}
@@ -83,6 +166,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
}
_getProxies(
BuildContext context,
List<Proxy> proxies,
ProxiesSortType proxiesSortType,
) {
@@ -102,7 +186,8 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
8 * 2;
}
_card({
_card(
BuildContext context, {
required void Function() onPressed,
required bool isSelected,
required Proxy proxy,
@@ -168,7 +253,9 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
SizedBox(
height: measure.labelSmallHeight,
child: Selector<AppState, int?>(
selector: (context, appState) => appState.getDelay(proxy.name),
selector: (context, appState) => appState.getDelay(
proxy.name,
),
builder: (_, delay, __) {
return FadeBox(
child: Builder(
@@ -222,13 +309,36 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
itemCount: proxies.length,
itemBuilder: (_, index) {
final proxy = proxies[index];
return Selector<AppState, bool>(
selector: (_, appState) => proxy.name == appState.currentProxyName,
builder: (_, isSelected, __) {
return _card(
return Selector3<AppState, Config, ClashConfig,
ProxiesCardSelectorState>(
selector: (_, appState, config, clashConfig) {
final group = appState.getGroupWithName(groupName)!;
bool isSelected = config.currentSelectedMap[group.name] == proxy.name ||
(config.currentSelectedMap[group.name] == null &&
group.now == proxy.name);
return ProxiesCardSelectorState(
isSelected: isSelected,
);
},
builder: (_, state, __) {
return _card(
context,
isSelected: state.isSelected,
onPressed: () {
context.appController.config.currentProxyName = proxy.name;
final appController = context.appController;
final group =
appController.appState.getGroupWithName(groupName)!;
if (group.type != GroupType.Selector) {
globalState.showSnackBar(
context,
message: "当前代理组无法选择",
);
return;
}
context.appController.config.updateCurrentSelectedMap(
groupName,
proxy.name,
);
context.appController.changeProxy();
},
proxy: proxy,
@@ -241,63 +351,52 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
@override
Widget build(BuildContext context) {
return DelayTestButtonContainer(
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
return ProxiesSelectorState(
proxiesSortType: config.proxiesSortType,
sortNum: appState.sortNum,
group: appState.currentGroup,
);
},
builder: (_, state, __) {
if (state.group == null) return Container();
final proxies = _getProxies(
state.group!.all,
state.proxiesSortType,
);
return Align(
alignment: Alignment.topCenter,
child: SlotLayout(
config: {
Breakpoints.small: SlotLayout.from(
key: const Key('proxies_grid_small'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 2,
),
),
Breakpoints.medium: SlotLayout.from(
key: const Key('proxies_grid_medium'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 3,
),
),
Breakpoints.large: SlotLayout.from(
key: const Key('proxies_grid_large'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 4,
),
),
},
return Selector2<AppState, Config, ProxiesTabViewSelectorState>(
selector: (_, appState, config) {
return ProxiesTabViewSelectorState(
proxiesSortType: config.proxiesSortType,
sortNum: appState.sortNum,
group: appState.getGroupWithName(groupName)!,
);
},
builder: (_, state, __) {
final proxies = _getProxies(
context,
state.group.all,
state.proxiesSortType,
);
return Align(
alignment: Alignment.topCenter,
child: SlotLayout(
config: {
Breakpoints.small: SlotLayout.from(
key: const Key('proxies_grid_small'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 2,
),
),
);
},
),
),
Breakpoints.medium: SlotLayout.from(
key: const Key('proxies_grid_medium'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 3,
),
),
Breakpoints.large: SlotLayout.from(
key: const Key('proxies_grid_large'),
builder: (_) => _buildGrid(
context,
proxies: proxies,
columns: 4,
),
),
},
),
);
},
);
}
}

View File

@@ -19,7 +19,8 @@ Future<void> main() async {
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState(
mode: clashConfig.mode,
currentProxyName: config.currentProxyName,
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
);
await globalState.init(
appState: appState,
@@ -46,7 +47,8 @@ Future<void> vpnService() async {
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState(
mode: clashConfig.mode,
currentProxyName: config.currentProxyName,
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
);
clashMessage.addListener(ClashMessageListenerWithVpn(onTun: (String fd) {
proxyManager.setProtect(

View File

@@ -5,39 +5,47 @@ import 'ffi.dart';
import 'log.dart';
import 'navigation.dart';
import 'package.dart';
import 'profile.dart';
import 'proxy.dart';
import 'system_color_scheme.dart';
import 'traffic.dart';
import 'version.dart';
typedef DelayMap = Map<String, int?>;
class AppState with ChangeNotifier {
List<NavigationItem> _navigationItems;
int? _runTime;
bool _isInit;
DelayMap _delayMap;
VersionInfo? _versionInfo;
List<Traffic> _traffics;
List<Log> _logs;
List<Package> _packages;
String _currentLabel;
SystemColorSchemes _systemColorSchemes;
List<Group> _groups;
num _sortNum;
Mode _mode;
String? _currentProxyName;
DelayMap _delayMap;
SelectedMap _selectedMap;
bool _isCompatible;
List<Group> _groups;
AppState({required Mode mode, required currentProxyName})
: _navigationItems = [],
_delayMap = {},
AppState({
required Mode mode,
required bool isCompatible,
required SelectedMap selectedMap,
}) : _navigationItems = [],
_isInit = false,
_currentLabel = "dashboard",
_traffics = [],
_logs = [],
_groups = [],
_selectedMap = selectedMap,
_packages = [],
_sortNum = 0,
_mode = mode,
_currentProxyName = currentProxyName,
_delayMap = {},
_groups = [],
_isCompatible = isCompatible,
_systemColorSchemes = SystemColorSchemes();
String get currentLabel => _currentLabel;
@@ -76,49 +84,38 @@ class AppState with ChangeNotifier {
}
}
DelayMap get delayMap => _delayMap;
set delayMap(DelayMap value) {
if (_delayMap != value) {
_delayMap = value;
notifyListeners();
}
}
String getDesc(String type, String? proxyName) {
final groupTypeNamesList = GroupType.values.map((e) => e.name).toList();
if (!groupTypeNamesList.contains(type)) {
return type;
}else{
} else {
final index = groups.indexWhere((element) => element.name == proxyName);
if(index == -1) return type;
if (index == -1) return type;
return "$type(${groups[index].now})";
}
}
int? getDelay(String? proxyName) {
String? getRealProxyName(String? proxyName) {
if (proxyName == null) return null;
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return _delayMap[proxyName];
if (index == -1) return proxyName;
final group = groups[index];
if (group.now == null) return null;
return _delayMap[group.now];
return getRealProxyName(selectedMap.containsKey(proxyName)
? selectedMap[proxyName]
: group.now);
}
String? get realCurrentProxyName {
if (currentProxyName == null) return null;
final index = groups.indexWhere((element) => element.name == currentProxyName);
if (index == -1) return currentProxyName;
final group = groups[index];
if (group.now == null) return null;
return group.now;
}
setDelay(Delay delay) {
if (_delayMap[delay.name] != delay.value) {
_delayMap = Map.from(_delayMap)..[delay.name] = delay.value;
notifyListeners();
String? get showProxyName {
if (currentGroups.isEmpty) {
return UsedProxy.DIRECT.name;
}
final firstGroup = currentGroups.first;
final firstGroupName = firstGroup.name;
return selectedMap[firstGroupName] ?? firstGroup.now;
}
int? getDelay(String? proxyName) {
return _delayMap[getRealProxyName(proxyName)];
}
VersionInfo? get versionInfo => _versionInfo;
@@ -203,39 +200,77 @@ class AppState with ChangeNotifier {
}
}
String? get currentProxyName {
if (mode == Mode.direct) return UsedProxy.DIRECT.name;
if (_currentProxyName != null) return _currentProxyName!;
return currentGroup?.now;
// String? get currentProxyName {
// if (mode == Mode.direct) return UsedProxy.DIRECT.name;
// if (_currentProxyName != null) return _currentProxyName!;
// return currentGroup?.now;
// }
//
// set currentProxyName(String? value) {
// if (_currentProxyName != value) {
// _currentProxyName = value;
// notifyListeners();
// }
// }
bool get isCompatible {
return _isCompatible;
}
set currentProxyName(String? value) {
if (_currentProxyName != value) {
_currentProxyName = value;
set isCompatible(bool value) {
if (_isCompatible != value) {
_isCompatible = value;
notifyListeners();
}
}
Group? get currentGroup {
switch (mode) {
case Mode.direct:
return null;
case Mode.global:
return globalGroup;
case Mode.rule:
return ruleGroup;
SelectedMap get selectedMap {
return _selectedMap;
}
set selectedMap(SelectedMap value) {
if (!const MapEquality<String, String>().equals(_selectedMap, value)) {
_selectedMap = value;
notifyListeners();
}
}
Group? get globalGroup {
final index =
groups.indexWhere((element) => element.name == GroupName.GLOBAL.name);
return index != -1 ? groups[index] : null;
List<Group> get currentGroups {
switch (mode) {
case Mode.direct:
return [];
case Mode.global:
return groups
.where((element) => element.name == GroupName.GLOBAL.name)
.toList();
case Mode.rule:
return groups
.where((element) => element.name != GroupName.GLOBAL.name)
.toList();
}
}
Group? get ruleGroup {
DelayMap get delayMap {
return _delayMap;
}
set delayMap(DelayMap value) {
if (!const MapEquality<String, int?>().equals(_delayMap, value)) {
_delayMap = value;
notifyListeners();
}
}
setDelay(Delay delay) {
if (_delayMap[delay.name] != delay.value) {
_delayMap = Map.from(_delayMap)..[delay.name] = delay.value;
notifyListeners();
}
}
Group? getGroupWithName(String groupName) {
final index =
groups.indexWhere((element) => element.name == GroupName.Proxy.name);
return index != -1 ? groups[index] : null;
currentGroups.indexWhere((element) => element.name == groupName);
return index != -1 ? currentGroups[index] : null;
}
}

View File

@@ -61,6 +61,7 @@ class AccessControl {
@JsonSerializable()
class Config extends ChangeNotifier {
List<Profile> _profiles;
bool _isCompatible;
String? _currentProfileId;
bool _autoLaunch;
bool _silentLaunch;
@@ -82,6 +83,7 @@ class Config extends ChangeNotifier {
_autoRun = false,
_themeMode = ThemeMode.system,
_openLog = false,
_isCompatible = false,
_primaryColor = appConstant.defaultPrimaryColor.value,
_proxiesSortType = ProxiesSortType.none,
_isMinimizeOnExit = true,
@@ -159,11 +161,15 @@ class Config extends ChangeNotifier {
}
}
String? get currentProxyName => currentProfile?.proxyName;
set currentProxyName(String? value){
if (currentProfile?.proxyName != value) {
currentProfile?.proxyName = value;
SelectedMap get currentSelectedMap {
return currentProfile?.selectedMap ?? {};
}
updateCurrentSelectedMap(String groupName, String proxyName) {
if (currentProfile?.selectedMap[groupName] != proxyName) {
currentProfile?.selectedMap = Map.from(currentProfile?.selectedMap ?? {})
..[groupName] = proxyName;
notifyListeners();
}
}
@@ -294,6 +300,18 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: false)
bool get isCompatible {
return _isCompatible;
}
set isCompatible(bool value) {
if (_isCompatible != value) {
_isCompatible = value;
notifyListeners();
}
}
update() {
notifyListeners();
}

View File

@@ -13,7 +13,8 @@ class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({
@JsonKey(name: "profile-path") String? profilePath,
required ClashConfig config,
@JsonKey(name: "is-patch") bool? isPatch,
@JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible,
}) = _UpdateConfigParams;
factory UpdateConfigParams.fromJson(Map<String, Object?> json) =>

View File

@@ -55,7 +55,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isAccessControl = json['isAccessControl'] as bool? ?? false
..accessControl =
AccessControl.fromJson(json['accessControl'] as Map<String, dynamic>)
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true;
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? false;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -72,6 +73,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAccessControl': instance.isAccessControl,
'accessControl': instance.accessControl,
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
};
const _$ThemeModeEnumMap = {

View File

@@ -24,7 +24,9 @@ mixin _$UpdateConfigParams {
String? get profilePath => throw _privateConstructorUsedError;
ClashConfig get config => throw _privateConstructorUsedError;
@JsonKey(name: "is-patch")
bool? get isPatch => throw _privateConstructorUsedError;
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -41,7 +43,8 @@ abstract class $UpdateConfigParamsCopyWith<$Res> {
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
@JsonKey(name: "is-patch") bool? isPatch});
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
}
/// @nodoc
@@ -59,7 +62,8 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? isPatch = freezed,
Object? isPatch = null,
Object? isCompatible = null,
}) {
return _then(_value.copyWith(
profilePath: freezed == profilePath
@@ -70,10 +74,14 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
isPatch: freezed == isPatch
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool?,
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
@@ -89,7 +97,8 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res>
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
@JsonKey(name: "is-patch") bool? isPatch});
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
}
/// @nodoc
@@ -105,7 +114,8 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? isPatch = freezed,
Object? isPatch = null,
Object? isCompatible = null,
}) {
return _then(_$UpdateConfigParamsImpl(
profilePath: freezed == profilePath
@@ -116,10 +126,14 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
isPatch: freezed == isPatch
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool?,
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -130,7 +144,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
const _$UpdateConfigParamsImpl(
{@JsonKey(name: "profile-path") this.profilePath,
required this.config,
@JsonKey(name: "is-patch") this.isPatch});
@JsonKey(name: "is-patch") required this.isPatch,
@JsonKey(name: "is-compatible") required this.isCompatible});
factory _$UpdateConfigParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateConfigParamsImplFromJson(json);
@@ -142,11 +157,14 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
final ClashConfig config;
@override
@JsonKey(name: "is-patch")
final bool? isPatch;
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
@override
String toString() {
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch)';
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch, isCompatible: $isCompatible)';
}
@override
@@ -157,12 +175,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
(identical(other.profilePath, profilePath) ||
other.profilePath == profilePath) &&
(identical(other.config, config) || other.config == config) &&
(identical(other.isPatch, isPatch) || other.isPatch == isPatch));
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) &&
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, profilePath, config, isPatch);
int get hashCode =>
Object.hash(runtimeType, profilePath, config, isPatch, isCompatible);
@JsonKey(ignore: true)
@override
@@ -183,7 +204,8 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
const factory _UpdateConfigParams(
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
@JsonKey(name: "is-patch") final bool? isPatch}) =
@JsonKey(name: "is-patch") required final bool isPatch,
@JsonKey(name: "is-compatible") required final bool isCompatible}) =
_$UpdateConfigParamsImpl;
factory _UpdateConfigParams.fromJson(Map<String, dynamic> json) =
@@ -196,7 +218,10 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
ClashConfig get config;
@override
@JsonKey(name: "is-patch")
bool? get isPatch;
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
@override
@JsonKey(ignore: true)
_$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith =>

View File

@@ -11,7 +11,8 @@ _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
_$UpdateConfigParamsImpl(
profilePath: json['profile-path'] as String?,
config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>),
isPatch: json['is-patch'] as bool?,
isPatch: json['is-patch'] as bool,
isCompatible: json['is-compatible'] as bool,
);
Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
@@ -20,6 +21,7 @@ Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
'profile-path': instance.profilePath,
'config': instance.config,
'is-patch': instance.isPatch,
'is-compatible': instance.isCompatible,
};
_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson(

View File

@@ -31,6 +31,9 @@ Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
lastUpdateDate: json['lastUpdateDate'] == null
? null
: DateTime.parse(json['lastUpdateDate'] as String),
selectedMap: (json['selectedMap'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
autoUpdateDuration: json['autoUpdateDuration'] == null
? null
: Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()),
@@ -46,4 +49,5 @@ Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
'autoUpdateDuration': instance.autoUpdateDuration.inMicroseconds,
'userInfo': instance.userInfo,
'autoUpdate': instance.autoUpdate,
'selectedMap': instance.selectedMap,
};

View File

@@ -219,6 +219,7 @@ Proxy _$ProxyFromJson(Map<String, dynamic> json) {
mixin _$Proxy {
String get name => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
String? get now => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -230,7 +231,7 @@ abstract class $ProxyCopyWith<$Res> {
factory $ProxyCopyWith(Proxy value, $Res Function(Proxy) then) =
_$ProxyCopyWithImpl<$Res, Proxy>;
@useResult
$Res call({String name, String type});
$Res call({String name, String type, String? now});
}
/// @nodoc
@@ -248,6 +249,7 @@ class _$ProxyCopyWithImpl<$Res, $Val extends Proxy>
$Res call({
Object? name = null,
Object? type = null,
Object? now = freezed,
}) {
return _then(_value.copyWith(
name: null == name
@@ -258,6 +260,10 @@ class _$ProxyCopyWithImpl<$Res, $Val extends Proxy>
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
now: freezed == now
? _value.now
: now // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
@@ -269,7 +275,7 @@ abstract class _$$ProxyImplCopyWith<$Res> implements $ProxyCopyWith<$Res> {
__$$ProxyImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String type});
$Res call({String name, String type, String? now});
}
/// @nodoc
@@ -285,6 +291,7 @@ class __$$ProxyImplCopyWithImpl<$Res>
$Res call({
Object? name = null,
Object? type = null,
Object? now = freezed,
}) {
return _then(_$ProxyImpl(
name: null == name
@@ -295,6 +302,10 @@ class __$$ProxyImplCopyWithImpl<$Res>
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
now: freezed == now
? _value.now
: now // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
@@ -302,21 +313,21 @@ class __$$ProxyImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$ProxyImpl implements _Proxy {
const _$ProxyImpl({this.name = "", this.type = ""});
const _$ProxyImpl({required this.name, required this.type, this.now});
factory _$ProxyImpl.fromJson(Map<String, dynamic> json) =>
_$$ProxyImplFromJson(json);
@override
@JsonKey()
final String name;
@override
@JsonKey()
final String type;
@override
final String? now;
@override
String toString() {
return 'Proxy(name: $name, type: $type)';
return 'Proxy(name: $name, type: $type, now: $now)';
}
@override
@@ -325,12 +336,13 @@ class _$ProxyImpl implements _Proxy {
(other.runtimeType == runtimeType &&
other is _$ProxyImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type));
(identical(other.type, type) || other.type == type) &&
(identical(other.now, now) || other.now == now));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, name, type);
int get hashCode => Object.hash(runtimeType, name, type, now);
@JsonKey(ignore: true)
@override
@@ -347,7 +359,10 @@ class _$ProxyImpl implements _Proxy {
}
abstract class _Proxy implements Proxy {
const factory _Proxy({final String name, final String type}) = _$ProxyImpl;
const factory _Proxy(
{required final String name,
required final String type,
final String? now}) = _$ProxyImpl;
factory _Proxy.fromJson(Map<String, dynamic> json) = _$ProxyImpl.fromJson;
@@ -356,6 +371,8 @@ abstract class _Proxy implements Proxy {
@override
String get type;
@override
String? get now;
@override
@JsonKey(ignore: true)
_$$ProxyImplCopyWith<_$ProxyImpl> get copyWith =>
throw _privateConstructorUsedError;

View File

@@ -31,12 +31,14 @@ const _$GroupTypeEnumMap = {
};
_$ProxyImpl _$$ProxyImplFromJson(Map<String, dynamic> json) => _$ProxyImpl(
name: json['name'] as String? ?? "",
type: json['type'] as String? ?? "",
name: json['name'] as String,
type: json['type'] as String,
now: json['now'] as String?,
);
Map<String, dynamic> _$$ProxyImplToJson(_$ProxyImpl instance) =>
<String, dynamic>{
'name': instance.name,
'type': instance.type,
'now': instance.now,
};

View File

@@ -1724,7 +1724,6 @@ abstract class _HomeNavigationSelectorState
/// @nodoc
mixin _$ProxiesCardSelectorState {
String? get currentProxyName => throw _privateConstructorUsedError;
bool get isSelected => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -1738,7 +1737,7 @@ abstract class $ProxiesCardSelectorStateCopyWith<$Res> {
$Res Function(ProxiesCardSelectorState) then) =
_$ProxiesCardSelectorStateCopyWithImpl<$Res, ProxiesCardSelectorState>;
@useResult
$Res call({String? currentProxyName, bool isSelected});
$Res call({bool isSelected});
}
/// @nodoc
@@ -1755,14 +1754,9 @@ class _$ProxiesCardSelectorStateCopyWithImpl<$Res,
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isSelected = null,
}) {
return _then(_value.copyWith(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isSelected: null == isSelected
? _value.isSelected
: isSelected // ignore: cast_nullable_to_non_nullable
@@ -1780,7 +1774,7 @@ abstract class _$$ProxiesCardSelectorStateImplCopyWith<$Res>
__$$ProxiesCardSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String? currentProxyName, bool isSelected});
$Res call({bool isSelected});
}
/// @nodoc
@@ -1796,14 +1790,9 @@ class __$$ProxiesCardSelectorStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isSelected = null,
}) {
return _then(_$ProxiesCardSelectorStateImpl(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isSelected: null == isSelected
? _value.isSelected
: isSelected // ignore: cast_nullable_to_non_nullable
@@ -1815,17 +1804,14 @@ class __$$ProxiesCardSelectorStateImplCopyWithImpl<$Res>
/// @nodoc
class _$ProxiesCardSelectorStateImpl implements _ProxiesCardSelectorState {
const _$ProxiesCardSelectorStateImpl(
{required this.currentProxyName, required this.isSelected});
const _$ProxiesCardSelectorStateImpl({required this.isSelected});
@override
final String? currentProxyName;
@override
final bool isSelected;
@override
String toString() {
return 'ProxiesCardSelectorState(currentProxyName: $currentProxyName, isSelected: $isSelected)';
return 'ProxiesCardSelectorState(isSelected: $isSelected)';
}
@override
@@ -1833,14 +1819,12 @@ class _$ProxiesCardSelectorStateImpl implements _ProxiesCardSelectorState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxiesCardSelectorStateImpl &&
(identical(other.currentProxyName, currentProxyName) ||
other.currentProxyName == currentProxyName) &&
(identical(other.isSelected, isSelected) ||
other.isSelected == isSelected));
}
@override
int get hashCode => Object.hash(runtimeType, currentProxyName, isSelected);
int get hashCode => Object.hash(runtimeType, isSelected);
@JsonKey(ignore: true)
@override
@@ -1851,12 +1835,9 @@ class _$ProxiesCardSelectorStateImpl implements _ProxiesCardSelectorState {
}
abstract class _ProxiesCardSelectorState implements ProxiesCardSelectorState {
const factory _ProxiesCardSelectorState(
{required final String? currentProxyName,
required final bool isSelected}) = _$ProxiesCardSelectorStateImpl;
const factory _ProxiesCardSelectorState({required final bool isSelected}) =
_$ProxiesCardSelectorStateImpl;
@override
String? get currentProxyName;
@override
bool get isSelected;
@override
@@ -1867,9 +1848,7 @@ abstract class _ProxiesCardSelectorState implements ProxiesCardSelectorState {
/// @nodoc
mixin _$ProxiesSelectorState {
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
num get sortNum => throw _privateConstructorUsedError;
Group? get group => throw _privateConstructorUsedError;
List<String> get groupNames => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxiesSelectorStateCopyWith<ProxiesSelectorState> get copyWith =>
@@ -1882,9 +1861,7 @@ abstract class $ProxiesSelectorStateCopyWith<$Res> {
$Res Function(ProxiesSelectorState) then) =
_$ProxiesSelectorStateCopyWithImpl<$Res, ProxiesSelectorState>;
@useResult
$Res call({ProxiesSortType proxiesSortType, num sortNum, Group? group});
$GroupCopyWith<$Res>? get group;
$Res call({List<String> groupNames});
}
/// @nodoc
@@ -1901,37 +1878,15 @@ class _$ProxiesSelectorStateCopyWithImpl<$Res,
@pragma('vm:prefer-inline')
@override
$Res call({
Object? proxiesSortType = null,
Object? sortNum = null,
Object? group = freezed,
Object? groupNames = null,
}) {
return _then(_value.copyWith(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
group: freezed == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group?,
groupNames: null == groupNames
? _value.groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$GroupCopyWith<$Res>? get group {
if (_value.group == null) {
return null;
}
return $GroupCopyWith<$Res>(_value.group!, (value) {
return _then(_value.copyWith(group: value) as $Val);
});
}
}
/// @nodoc
@@ -1942,10 +1897,7 @@ abstract class _$$ProxiesSelectorStateImplCopyWith<$Res>
__$$ProxiesSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ProxiesSortType proxiesSortType, num sortNum, Group? group});
@override
$GroupCopyWith<$Res>? get group;
$Res call({List<String> groupNames});
}
/// @nodoc
@@ -1959,23 +1911,13 @@ class __$$ProxiesSelectorStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? proxiesSortType = null,
Object? sortNum = null,
Object? group = freezed,
Object? groupNames = null,
}) {
return _then(_$ProxiesSelectorStateImpl(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
group: freezed == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group?,
groupNames: null == groupNames
? _value._groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
@@ -1983,21 +1925,20 @@ class __$$ProxiesSelectorStateImplCopyWithImpl<$Res>
/// @nodoc
class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
const _$ProxiesSelectorStateImpl(
{required this.proxiesSortType,
required this.sortNum,
required this.group});
const _$ProxiesSelectorStateImpl({required final List<String> groupNames})
: _groupNames = groupNames;
final List<String> _groupNames;
@override
final ProxiesSortType proxiesSortType;
@override
final num sortNum;
@override
final Group? group;
List<String> get groupNames {
if (_groupNames is EqualUnmodifiableListView) return _groupNames;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_groupNames);
}
@override
String toString() {
return 'ProxiesSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group)';
return 'ProxiesSelectorState(groupNames: $groupNames)';
}
@override
@@ -2005,14 +1946,13 @@ class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxiesSelectorStateImpl &&
(identical(other.proxiesSortType, proxiesSortType) ||
other.proxiesSortType == proxiesSortType) &&
(identical(other.sortNum, sortNum) || other.sortNum == sortNum) &&
(identical(other.group, group) || other.group == group));
const DeepCollectionEquality()
.equals(other._groupNames, _groupNames));
}
@override
int get hashCode => Object.hash(runtimeType, proxiesSortType, sortNum, group);
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_groupNames));
@JsonKey(ignore: true)
@override
@@ -2025,18 +1965,190 @@ class _$ProxiesSelectorStateImpl implements _ProxiesSelectorState {
abstract class _ProxiesSelectorState implements ProxiesSelectorState {
const factory _ProxiesSelectorState(
{required final List<String> groupNames}) = _$ProxiesSelectorStateImpl;
@override
List<String> get groupNames;
@override
@JsonKey(ignore: true)
_$$ProxiesSelectorStateImplCopyWith<_$ProxiesSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxiesTabViewSelectorState {
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
num get sortNum => throw _privateConstructorUsedError;
Group get group => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxiesTabViewSelectorStateCopyWith<ProxiesTabViewSelectorState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProxiesTabViewSelectorStateCopyWith<$Res> {
factory $ProxiesTabViewSelectorStateCopyWith(
ProxiesTabViewSelectorState value,
$Res Function(ProxiesTabViewSelectorState) then) =
_$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
ProxiesTabViewSelectorState>;
@useResult
$Res call({ProxiesSortType proxiesSortType, num sortNum, Group group});
$GroupCopyWith<$Res> get group;
}
/// @nodoc
class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
$Val extends ProxiesTabViewSelectorState>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> {
_$ProxiesTabViewSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? proxiesSortType = null,
Object? sortNum = null,
Object? group = null,
}) {
return _then(_value.copyWith(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
group: null == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$GroupCopyWith<$Res> get group {
return $GroupCopyWith<$Res>(_value.group, (value) {
return _then(_value.copyWith(group: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProxiesTabViewSelectorStateImplCopyWith<$Res>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> {
factory _$$ProxiesTabViewSelectorStateImplCopyWith(
_$ProxiesTabViewSelectorStateImpl value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) then) =
__$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ProxiesSortType proxiesSortType, num sortNum, Group group});
@override
$GroupCopyWith<$Res> get group;
}
/// @nodoc
class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>
extends _$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
_$ProxiesTabViewSelectorStateImpl>
implements _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> {
__$$ProxiesTabViewSelectorStateImplCopyWithImpl(
_$ProxiesTabViewSelectorStateImpl _value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? proxiesSortType = null,
Object? sortNum = null,
Object? group = null,
}) {
return _then(_$ProxiesTabViewSelectorStateImpl(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
group: null == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group,
));
}
}
/// @nodoc
class _$ProxiesTabViewSelectorStateImpl
implements _ProxiesTabViewSelectorState {
const _$ProxiesTabViewSelectorStateImpl(
{required this.proxiesSortType,
required this.sortNum,
required this.group});
@override
final ProxiesSortType proxiesSortType;
@override
final num sortNum;
@override
final Group group;
@override
String toString() {
return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxiesTabViewSelectorStateImpl &&
(identical(other.proxiesSortType, proxiesSortType) ||
other.proxiesSortType == proxiesSortType) &&
(identical(other.sortNum, sortNum) || other.sortNum == sortNum) &&
(identical(other.group, group) || other.group == group));
}
@override
int get hashCode => Object.hash(runtimeType, proxiesSortType, sortNum, group);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl>
get copyWith => __$$ProxiesTabViewSelectorStateImplCopyWithImpl<
_$ProxiesTabViewSelectorStateImpl>(this, _$identity);
}
abstract class _ProxiesTabViewSelectorState
implements ProxiesTabViewSelectorState {
const factory _ProxiesTabViewSelectorState(
{required final ProxiesSortType proxiesSortType,
required final num sortNum,
required final Group? group}) = _$ProxiesSelectorStateImpl;
required final Group group}) = _$ProxiesTabViewSelectorStateImpl;
@override
ProxiesSortType get proxiesSortType;
@override
num get sortNum;
@override
Group? get group;
Group get group;
@override
@JsonKey(ignore: true)
_$$ProxiesSelectorStateImplCopyWith<_$ProxiesSelectorStateImpl>
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -12,6 +12,8 @@ import 'common.dart';
part 'generated/profile.g.dart';
typedef SelectedMap = Map<String, String>;
@JsonSerializable()
class UserInfo {
int upload;
@@ -68,6 +70,7 @@ class Profile {
Duration autoUpdateDuration;
UserInfo? userInfo;
bool autoUpdate;
SelectedMap selectedMap;
Profile({
String? id,
@@ -76,11 +79,13 @@ class Profile {
this.userInfo,
this.proxyName,
this.lastUpdateDate,
SelectedMap? selectedMap,
Duration? autoUpdateDuration,
this.autoUpdate = true,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
autoUpdateDuration =
autoUpdateDuration ?? appConstant.defaultUpdateDuration;
autoUpdateDuration ?? appConstant.defaultUpdateDuration,
selectedMap = selectedMap ?? {};
ProfileType get type => url == null ? ProfileType.file : ProfileType.url;
@@ -188,6 +193,7 @@ class Profile {
DateTime? lastUpdateDate,
Duration? autoUpdateDuration,
bool? autoUpdate,
SelectedMap? selectedMap,
}) {
return Profile(
id: id,
@@ -195,6 +201,7 @@ class Profile {
url: url ?? this.url,
proxyName: proxyName ?? this.proxyName,
userInfo: userInfo ?? this.userInfo,
selectedMap: selectedMap ?? this.selectedMap,
lastUpdateDate: lastUpdateDate ?? this.lastUpdateDate,
autoUpdateDuration: autoUpdateDuration ?? this.autoUpdateDuration,
autoUpdate: autoUpdate ?? this.autoUpdate,

View File

@@ -6,7 +6,7 @@ part 'generated/proxy.g.dart';
part 'generated/proxy.freezed.dart';
typedef DelayMap = Map<String, int?>;
typedef ProxyMap = Map<String, Proxy>;
@freezed
class Group with _$Group {
@@ -23,8 +23,9 @@ class Group with _$Group {
@freezed
class Proxy with _$Proxy {
const factory Proxy({
@Default("") String name,
@Default("") String type,
required String name,
required String type,
String? now,
}) = _Proxy;
factory Proxy.fromJson(Map<String, Object?> json) => _$ProxyFromJson(json);

View File

@@ -105,7 +105,6 @@ class HomeNavigationSelectorState with _$HomeNavigationSelectorState{
@freezed
class ProxiesCardSelectorState with _$ProxiesCardSelectorState{
const factory ProxiesCardSelectorState({
required String? currentProxyName,
required bool isSelected,
}) = _ProxiesCardSelectorState;
}
@@ -113,8 +112,15 @@ class ProxiesCardSelectorState with _$ProxiesCardSelectorState{
@freezed
class ProxiesSelectorState with _$ProxiesSelectorState{
const factory ProxiesSelectorState({
required ProxiesSortType proxiesSortType,
required num sortNum,
required Group? group,
required List<String> groupNames,
}) = _ProxiesSelectorState;
}
@freezed
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState{
const factory ProxiesTabViewSelectorState({
required ProxiesSortType proxiesSortType,
required num sortNum,
required Group group,
}) = _ProxiesTabViewSelectorState;
}

View File

@@ -15,6 +15,8 @@ import 'common/common.dart';
class GlobalState {
Timer? timer;
Function? updateSortNumDebounce;
Timer? groupsUpdateTimer;
Function? updateCurrentDelayDebounce;
PageController? pageController;
final navigatorKey = GlobalKey<NavigatorState>();
final Map<int, String?> packageNameMap = {};
@@ -22,7 +24,6 @@ class GlobalState {
List<Function> updateFunctionLists = [];
List<NavigationItem> currentNavigationItems = [];
bool updatePackagesLock = false;
bool healthcheckLock = false;
startListenUpdate() {
if (timer != null && timer!.isActive == true) return;
@@ -49,6 +50,7 @@ class GlobalState {
profilePath: profilePath,
config: clashConfig,
isPatch: isPatch,
isCompatible: config.isCompatible,
));
}
@@ -74,7 +76,6 @@ class GlobalState {
stopListenUpdate();
}
applyProfile({
required AppState appState,
required Config config,
@@ -125,30 +126,14 @@ class GlobalState {
stopSystemProxy();
return;
}
final currentProxyName = appState.currentProxyName;
if (currentProxyName == null) return;
final index1 = appState.globalGroup?.all.indexWhere(
(element) => element.name == currentProxyName,
);
if (index1 != null && index1 != -1) {
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: GroupName.GLOBAL.name,
proxyName: currentProxyName,
groupName: key,
proxyName: value,
),
);
}
final index2 = appState.ruleGroup?.all.indexWhere(
(element) => element.name == currentProxyName,
);
if (index2 != null && index2 != -1) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: GroupName.Proxy.name,
proxyName: currentProxyName,
),
);
}
});
});
}
@@ -165,11 +150,11 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) {
final group = appState.currentGroup;
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: group != null && hasProfile,
hasProxies: group.isNotEmpty && hasProfile,
);
}
@@ -253,6 +238,52 @@ class GlobalState {
);
}
}
showSnackBar(
BuildContext context, {
required String message,
SnackBarAction? action,
}) {
final width = context.width;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 96,
right: 16,
left: 16,
);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: action,
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
margin: margin,
),
);
}
void updateCurrentDelay(
String? proxyName,
) {
updateCurrentDelayDebounce ??= debounce<Function(String?)>((proxyName) {
if (proxyName != null) {
debugPrint("[delay]=====> $proxyName");
clashCore.delay(
proxyName,
);
}
});
updateCurrentDelayDebounce!([proxyName]);
}
}
final globalState = GlobalState();

View File

@@ -25,11 +25,11 @@ class AppStateContainer extends StatelessWidget {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
final group = appState.currentGroup;
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
return UpdateNavigationsSelector(
openLogs: config.openLogs,
hasProxies: group != null && hasProfile,
hasProxies: group.isNotEmpty && hasProfile,
);
},
builder: (context, state, child) {

View File

@@ -40,24 +40,11 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
void onDelay(Delay delay) {
final appController = context.appController;
appController.setDelay(delay);
globalState.healthcheckLock = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.updateSortNumDebounce ??= debounce<Function()>(
() async {
await appController.updateGroups();
appController.appState.sortNum++;
globalState.healthcheckLock = false;
},
milliseconds: 5000,
);
globalState.updateSortNumDebounce!();
});
super.onDelay(delay);
}
@override
void onLog(Log log) {
print("$log");
context.appController.appState.addLog(log);
super.onLog(log);
}

View File

@@ -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.7.14
version: 0.8.0
environment:
sdk: '>=3.1.0 <4.0.0'