Compare commits
1 Commits
v0.8.92-pr
...
v0.8.92-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5113733ab2 |
@@ -1,7 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="additionalArgs" value="--dart-define-from-file env.json" />
|
||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_app:
|
||||
dart ./setup.dart android
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
dart ./setup.dart macos --arch arm64 --out core
|
||||
@@ -64,7 +64,7 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".dev"
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
|
||||
release {
|
||||
|
||||
Submodule core/Clash.Meta updated: c27f82fbdf...4d4492c38c
40
core/hub.go
40
core/hub.go
@@ -99,47 +99,27 @@ func handleValidateConfig(path string) string {
|
||||
func handleGetProxies() ProxiesData {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
|
||||
nameList := config.GetProxyNameList()
|
||||
|
||||
var all = []string{"GLOBAL"}
|
||||
all = append(all, config.ProxyList...)
|
||||
proxies := make(map[string]constant.Proxy)
|
||||
|
||||
for name, proxy := range tunnel.Proxies() {
|
||||
proxies[name] = proxy
|
||||
}
|
||||
for _, p := range tunnel.Providers() {
|
||||
for _, proxy := range p.Proxies() {
|
||||
proxies[proxy.Name()] = proxy
|
||||
name := proxy.Name()
|
||||
proxies[name] = proxy
|
||||
}
|
||||
}
|
||||
|
||||
hasGlobal := false
|
||||
allNames := make([]string, 0, len(nameList)+1)
|
||||
|
||||
for _, name := range nameList {
|
||||
if name == "GLOBAL" {
|
||||
hasGlobal = true
|
||||
}
|
||||
|
||||
p, ok := proxies[name]
|
||||
if !ok || p == nil {
|
||||
continue
|
||||
}
|
||||
switch p.Type() {
|
||||
case constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance:
|
||||
allNames = append(allNames, name)
|
||||
default:
|
||||
}
|
||||
types := []constant.AdapterType{
|
||||
constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance,
|
||||
}
|
||||
|
||||
if !hasGlobal {
|
||||
if p, ok := proxies["GLOBAL"]; ok && p != nil {
|
||||
allNames = append([]string{"GLOBAL"}, allNames...)
|
||||
}
|
||||
}
|
||||
|
||||
nextAll := slices.DeleteFunc(all, func(name string) bool {
|
||||
return !slices.Contains(types, proxies[name].Type())
|
||||
})
|
||||
return ProxiesData{
|
||||
All: allNames,
|
||||
All: nextAll,
|
||||
Proxies: proxies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,35 +14,20 @@ class Migration {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<Config> migrationIfNeeded(
|
||||
Map<String, Object?>? configMap, {
|
||||
required Future<Config> Function(MigrationData data) sync,
|
||||
}) async {
|
||||
Future<MigrationData> migrationIfNeeded(
|
||||
Map<String, Object?>? configMap,
|
||||
) async {
|
||||
_oldVersion = await preferences.getVersion();
|
||||
if (_oldVersion == currentVersion) {
|
||||
try {
|
||||
return Config.realFromJson(configMap);
|
||||
} catch (_) {
|
||||
final isV0 = configMap?['proxiesStyle'] != null;
|
||||
if (isV0) {
|
||||
_oldVersion = 0;
|
||||
} else {
|
||||
throw 'Local data is damaged. A reset is required to fix this issue.';
|
||||
}
|
||||
}
|
||||
}
|
||||
MigrationData data = MigrationData(configMap: configMap);
|
||||
if (_oldVersion == 0 && configMap != null) {
|
||||
final clashConfigMap = await preferences.getClashConfigMap();
|
||||
if (clashConfigMap != null) {
|
||||
configMap['patchClashConfig'] = clashConfigMap;
|
||||
await preferences.clearClashConfig();
|
||||
}
|
||||
data = await _oldToNow(configMap);
|
||||
}
|
||||
final res = await sync(data);
|
||||
await preferences.setVersion(currentVersion);
|
||||
return res;
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> rollback() async {
|
||||
await preferences.setVersion(_oldVersion);
|
||||
}
|
||||
|
||||
Future<MigrationData> _oldToNow(Map<String, Object?> configMap) async {
|
||||
|
||||
@@ -40,36 +40,11 @@ class Preferences {
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> getConfigMap() async {
|
||||
try {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final Map<String, Object?>? configMap = json.decode(configString);
|
||||
return configMap;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> getClashConfigMap() async {
|
||||
try {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
if (clashConfigString == null) return null;
|
||||
return json.decode(clashConfigString);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearClashConfig() async {
|
||||
try {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
await preferences?.remove(clashConfigKey);
|
||||
return;
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final Map<String, Object?>? configMap = json.decode(configString);
|
||||
return configMap;
|
||||
}
|
||||
|
||||
Future<Config?> getConfig() async {
|
||||
|
||||
@@ -16,9 +16,6 @@ class CommonPrint {
|
||||
void log(String? text, {LogLevel logLevel = LogLevel.info}) {
|
||||
final payload = '[APP] $text';
|
||||
debugPrint(payload);
|
||||
if (!appController.isAttach) {
|
||||
return;
|
||||
}
|
||||
appController.addLog(Log.app(payload).copyWith(logLevel: logLevel));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +402,6 @@ Future<MigrationData> _oldToNowTask(
|
||||
final addedRules = standardOverwrite['addedRules'] as List? ?? [];
|
||||
for (final addRule in addedRules) {
|
||||
final id = idMap.updateCacheValue(addRule['id'], () => snowflake.id);
|
||||
addRule['id'] = id;
|
||||
rules.add(Rule.fromJson(addRule));
|
||||
links.add(
|
||||
ProfileRuleLink(
|
||||
@@ -431,9 +430,9 @@ Future<MigrationData> _oldToNowTask(
|
||||
final scriptOverwrite = overwrite['scriptOverwrite'] as Map?;
|
||||
if (scriptOverwrite != null) {
|
||||
final scriptId = scriptOverwrite['scriptId'] as String?;
|
||||
rawProfile['scriptId'] = scriptId != null ? idMap[scriptId] : null;
|
||||
configMap['scriptId'] = scriptId != null ? idMap[scriptId] : null;
|
||||
}
|
||||
rawProfile['overwriteType'] = overwrite['type'];
|
||||
configMap['overwriteType'] = overwrite['type'];
|
||||
}
|
||||
|
||||
final sourceFile = File(_getProfilePath(sourcePath, rawId));
|
||||
@@ -443,8 +442,9 @@ Future<MigrationData> _oldToNowTask(
|
||||
}
|
||||
final currentProfileId = configMap['currentProfileId'];
|
||||
configMap['currentProfileId'] = currentProfileId != null
|
||||
? idMap[currentProfileId]
|
||||
? idMap[currentProfileId.toString()]
|
||||
: null;
|
||||
|
||||
return MigrationData(
|
||||
configMap: configMap,
|
||||
profiles: profiles,
|
||||
@@ -576,14 +576,14 @@ Future<MigrationData> _restoreTask(RootIsolateToken token) async {
|
||||
final scripts = results[1].cast<Script>();
|
||||
final profilesMigration = profiles.map(
|
||||
(item) => VM2(
|
||||
_getProfilePath(restoreDirPath, item.id.toString()),
|
||||
_getProfilePath(homeDirPath, item.id.toString()),
|
||||
_getProfilePath(restoreDirPath, item.fileName),
|
||||
_getProfilePath(homeDirPath, item.fileName),
|
||||
),
|
||||
);
|
||||
final scriptsMigration = scripts.map(
|
||||
(item) => VM2(
|
||||
_getScriptPath(restoreDirPath, item.id.toString()),
|
||||
_getScriptPath(homeDirPath, item.id.toString()),
|
||||
_getScriptPath(restoreDirPath, item.fileName),
|
||||
_getScriptPath(homeDirPath, item.fileName),
|
||||
),
|
||||
);
|
||||
await _copyWithMapList([...profilesMigration, ...scriptsMigration]);
|
||||
|
||||
@@ -26,10 +26,6 @@ class Tray {
|
||||
return system.isWindows ? 'ico' : 'png';
|
||||
}
|
||||
|
||||
Future<void> destroy() async {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
|
||||
String getTryIcon({required bool isStart, required bool tunEnable}) {
|
||||
if (system.isMacOS || !isStart) {
|
||||
return 'assets/images/icon/status_1.$trayIconSuffix';
|
||||
|
||||
@@ -83,7 +83,6 @@ class Window {
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await windowManager.close();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -19,7 +20,6 @@ import 'providers/database.dart';
|
||||
class AppController {
|
||||
late final BuildContext _context;
|
||||
late final WidgetRef _ref;
|
||||
bool isAttach = false;
|
||||
|
||||
static AppController? _instance;
|
||||
|
||||
@@ -34,7 +34,6 @@ class AppController {
|
||||
_context = context;
|
||||
_ref = ref;
|
||||
await _init();
|
||||
isAttach = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,22 +45,26 @@ extension InitControllerExt on AppController {
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
};
|
||||
updateTray();
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch);
|
||||
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
try {
|
||||
updateTray();
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch);
|
||||
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
await _handleFailedPreference();
|
||||
await _handlerDisclaimer();
|
||||
await _showCrashlyticsTip();
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
} catch (e) {
|
||||
commonPrint.log('init error: $e');
|
||||
}
|
||||
await _handleFailedPreference();
|
||||
await _handlerDisclaimer();
|
||||
await _showCrashlyticsTip();
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
}
|
||||
|
||||
Future<void> _handleFailedPreference() async {
|
||||
@@ -138,11 +141,6 @@ extension InitControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<void> _initStatus() async {
|
||||
if (!globalState.needInitStatus) {
|
||||
commonPrint.log('init status cancel');
|
||||
return;
|
||||
}
|
||||
commonPrint.log('init status');
|
||||
if (system.isAndroid) {
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
@@ -555,18 +553,18 @@ extension SetupControllerExt on AppController {
|
||||
|
||||
Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
|
||||
if (isStart) {
|
||||
final res = await tryStartCore(true);
|
||||
if (res) {
|
||||
return;
|
||||
}
|
||||
if (!isInit) {
|
||||
final res = await tryStartCore(true);
|
||||
if (res) {
|
||||
return;
|
||||
}
|
||||
if (!_ref.read(initProvider)) {
|
||||
return;
|
||||
}
|
||||
await globalState.handleStart([updateRunTime, updateTraffic]);
|
||||
applyProfileDebounce(force: true, silence: true);
|
||||
} else {
|
||||
globalState.needInitStatus = false;
|
||||
_ref.read(runTimeProvider.notifier).value = 0;
|
||||
await applyProfile(
|
||||
force: true,
|
||||
preloadInvoke: () async {
|
||||
@@ -614,18 +612,6 @@ extension SetupControllerExt on AppController {
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
}
|
||||
|
||||
void tryCheckIp() {
|
||||
final isTimeout = _ref.read(
|
||||
networkDetectionProvider.select(
|
||||
(state) => state.ipInfo == null && state.isLoading == false,
|
||||
),
|
||||
);
|
||||
if (!isTimeout) {
|
||||
return;
|
||||
}
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
}
|
||||
|
||||
void applyProfileDebounce({bool silence = false, bool force = false}) {
|
||||
debouncer.call(FunctionTag.applyProfile, (silence, force) {
|
||||
applyProfile(silence: silence, force: force);
|
||||
@@ -732,7 +718,6 @@ extension SetupControllerExt on AppController {
|
||||
if (!force && !await needSetup()) {
|
||||
return;
|
||||
}
|
||||
commonPrint.log('setup ===>');
|
||||
var profile = _ref.read(currentProfileProvider);
|
||||
final nextProfile = await profile?.checkAndUpdateAndCopy();
|
||||
if (nextProfile != null) {
|
||||
@@ -812,6 +797,9 @@ extension CoreControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<Result<bool>> _requestAdmin(bool enableTun) async {
|
||||
if (system.isWindows && kDebugMode) {
|
||||
return Result.success(false);
|
||||
}
|
||||
final realTunEnable = _ref.read(realTunEnableProvider);
|
||||
if (enableTun != realTunEnable && realTunEnable == false) {
|
||||
final code = await system.authorizeCore();
|
||||
@@ -831,8 +819,8 @@ extension CoreControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<void> restartCore([bool start = false]) async {
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
await coreController.shutdown(true);
|
||||
globalState.isUserDisconnected = true;
|
||||
await coreController.shutdown();
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
if (start || _ref.read(isStartProvider)) {
|
||||
@@ -846,7 +834,7 @@ extension CoreControllerExt on AppController {
|
||||
if (coreController.isCompleted) {
|
||||
return false;
|
||||
}
|
||||
await restartCore(start);
|
||||
await restartCore();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -872,13 +860,12 @@ extension SystemControllerExt on AppController {
|
||||
system.exit();
|
||||
});
|
||||
try {
|
||||
await Future.wait([
|
||||
if (needSave) preferences.saveConfig(config),
|
||||
if (macOS != null) macOS!.updateDns(true),
|
||||
if (proxy != null) proxy!.stopProxy(),
|
||||
coreController.destroy(),
|
||||
if (tray != null) tray!.destroy(),
|
||||
]);
|
||||
if (needSave) {
|
||||
await preferences.saveConfig(config);
|
||||
}
|
||||
await proxy?.stopProxy();
|
||||
await macOS?.updateDns(true);
|
||||
await coreController.destroy();
|
||||
commonPrint.log('exit');
|
||||
} finally {
|
||||
system.exit();
|
||||
@@ -1193,8 +1180,8 @@ extension CommonControllerExt on AppController {
|
||||
}
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e, s) {
|
||||
commonPrint.log('$title ===> $e, $s', logLevel: LogLevel.warning);
|
||||
} catch (e) {
|
||||
commonPrint.log('$title===> $e', logLevel: LogLevel.warning);
|
||||
if (silence) {
|
||||
globalState.showNotifier(e.toString());
|
||||
} else {
|
||||
|
||||
@@ -65,8 +65,8 @@ class CoreController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> shutdown(bool isUser) async {
|
||||
await _interface.shutdown(isUser);
|
||||
Future<void> shutdown() async {
|
||||
await _interface.shutdown();
|
||||
}
|
||||
|
||||
FutureOr<bool> get isInit => _interface.isInit;
|
||||
@@ -201,10 +201,9 @@ class CoreController {
|
||||
final profilePath = await appPath.getProfilePath(id.toString());
|
||||
final res = await _interface.getConfig(profilePath);
|
||||
if (res.isSuccess) {
|
||||
final data = Map<String, dynamic>.from(res.data);
|
||||
data['rules'] = data['rule'];
|
||||
data.remove('rule');
|
||||
return data;
|
||||
res.data['rules'] = res.data['rule'];
|
||||
res.data.remove('rule');
|
||||
return res.data;
|
||||
} else {
|
||||
throw res.message;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ mixin CoreInterface {
|
||||
|
||||
Future<String> preload();
|
||||
|
||||
Future<bool> shutdown(bool isUser);
|
||||
Future<bool> shutdown();
|
||||
|
||||
Future<bool> get isInit;
|
||||
|
||||
@@ -86,9 +86,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
try {
|
||||
if (!completer.isCompleted) {
|
||||
return null;
|
||||
}
|
||||
await completer.future.timeout(const Duration(seconds: 10));
|
||||
} catch (e) {
|
||||
commonPrint.log(
|
||||
'Invoke pre ${method.name} timeout $e',
|
||||
@@ -100,9 +98,9 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
|
||||
}
|
||||
|
||||
return await utils.handleWatch(
|
||||
return utils.handleWatch(
|
||||
function: () async {
|
||||
return await invoke<T>(method: method, data: data, timeout: timeout);
|
||||
return await invoke(method: method, data: data, timeout: timeout);
|
||||
},
|
||||
onWatch: (data, elapsedMilliseconds) {
|
||||
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
|
||||
@@ -133,7 +131,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown(bool isUser);
|
||||
Future<bool> shutdown();
|
||||
|
||||
@override
|
||||
Future<bool> get isInit async {
|
||||
@@ -165,8 +163,8 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
|
||||
@override
|
||||
Future<Result> getConfig(String path) async {
|
||||
final res = await _invoke(method: ActionMethod.getConfig, data: path);
|
||||
return res ?? Result.success({});
|
||||
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ??
|
||||
Result.success({});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -37,12 +37,10 @@ class CoreLib extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown(_) async {
|
||||
if (!_connectedCompleter.isCompleted) {
|
||||
return false;
|
||||
}
|
||||
Future<bool> shutdown() async {
|
||||
await service?.shutdown();
|
||||
_connectedCompleter = Completer();
|
||||
return service?.shutdown() ?? true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,8 +16,6 @@ class CoreService extends CoreHandlerInterface {
|
||||
|
||||
Completer<Socket> _socketCompleter = Completer();
|
||||
|
||||
Completer<bool> _shutdownCompleter = Completer();
|
||||
|
||||
final Map<String, Completer> _callbackCompleterMap = {};
|
||||
|
||||
Process? _process;
|
||||
@@ -37,9 +35,6 @@ class CoreService extends CoreHandlerInterface {
|
||||
if (result.id?.isEmpty == true) {
|
||||
coreEventManager.sendEvent(CoreEvent.fromJson(result.data));
|
||||
}
|
||||
if (completer?.isCompleted == true) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(data);
|
||||
}
|
||||
|
||||
@@ -81,9 +76,6 @@ class CoreService extends CoreHandlerInterface {
|
||||
})
|
||||
.onDone(() {
|
||||
_handleInvokeCrashEvent();
|
||||
if (!_shutdownCompleter.isCompleted) {
|
||||
_shutdownCompleter.complete(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +87,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
|
||||
Future<void> start() async {
|
||||
if (_process != null) {
|
||||
await shutdown(false);
|
||||
await shutdown();
|
||||
}
|
||||
final serverSocket = await _serverCompleter.future;
|
||||
final arg = system.isWindows
|
||||
@@ -121,7 +113,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await _serverCompleter.future;
|
||||
await shutdown(false);
|
||||
await shutdown();
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
@@ -143,16 +135,12 @@ class CoreService extends CoreHandlerInterface {
|
||||
if (_socketCompleter.isCompleted) {
|
||||
final socket = await _socketCompleter.future;
|
||||
_socketCompleter = Completer();
|
||||
await socket.close();
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown(bool isUser) async {
|
||||
if (!_socketCompleter.isCompleted && _process == null) {
|
||||
return false;
|
||||
}
|
||||
_shutdownCompleter = Completer();
|
||||
shutdown() async {
|
||||
await _destroySocket();
|
||||
_clearCompleter();
|
||||
if (system.isWindows) {
|
||||
@@ -160,11 +148,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
_process?.kill();
|
||||
_process = null;
|
||||
if (isUser) {
|
||||
return _shutdownCompleter.future;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _clearCompleter() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/pages/error.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -10,22 +9,11 @@ import 'application.dart';
|
||||
import 'common/common.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final version = await system.version;
|
||||
final container = await globalState.init(version);
|
||||
HttpOverrides.global = FlClashHttpOverrides();
|
||||
runApp(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const Application(),
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
return runApp(
|
||||
MaterialApp(
|
||||
home: InitErrorScreen(error: e, stack: s),
|
||||
),
|
||||
);
|
||||
}
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final version = await system.version;
|
||||
final container = await globalState.init(version);
|
||||
HttpOverrides.global = FlClashHttpOverrides();
|
||||
runApp(
|
||||
UncontrolledProviderScope(container: container, child: const Application()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
render?.resume();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appController.tryCheckIp();
|
||||
if (system.isAndroid) {
|
||||
appController.addCheckIp();
|
||||
appController.tryStartCore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,14 +98,16 @@ class _CoreContainerState extends ConsumerState<CoreManager>
|
||||
|
||||
@override
|
||||
Future<void> onCrash(String message) async {
|
||||
if (!globalState.isUserDisconnected &&
|
||||
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
globalState.isUserDisconnected = false;
|
||||
if (ref.read(coreStatusProvider) != CoreStatus.connected) {
|
||||
return;
|
||||
}
|
||||
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
await coreController.shutdown(false);
|
||||
await coreController.shutdown();
|
||||
super.onCrash(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,11 +243,4 @@ abstract class Config with _$Config {
|
||||
}) = _Config;
|
||||
|
||||
factory Config.fromJson(Map<String, Object?> json) => _$ConfigFromJson(json);
|
||||
|
||||
factory Config.realFromJson(Map<String, Object?>? json) {
|
||||
if (json == null) {
|
||||
return Config(themeProps: defaultThemeProps);
|
||||
}
|
||||
return _$ConfigFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
@@ -127,7 +125,7 @@ class _EditorPageState extends ConsumerState<EditorPage> {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
final res = utf8.decode(file.bytes?.toList() ?? []);
|
||||
final res = String.fromCharCodes(file.bytes?.toList() ?? []);
|
||||
_controller.text = res;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import 'package:fl_clash/common/color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class InitErrorScreen extends StatelessWidget {
|
||||
final Object error;
|
||||
final StackTrace stack;
|
||||
|
||||
const InitErrorScreen({super.key, required this.error, required this.stack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Init Failed'),
|
||||
backgroundColor: colorScheme.error,
|
||||
foregroundColor: colorScheme.onError,
|
||||
elevation: 0,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.report_problem,
|
||||
color: colorScheme.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'The application encountered a critical error during startup and cannot continue.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionLabel('Error Details:'),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.opacity50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: colorScheme.error.opacity50),
|
||||
),
|
||||
child: SelectableText(
|
||||
error.toString(),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionLabel('Stack Trace:'),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.opacity50),
|
||||
),
|
||||
child: SelectableText(
|
||||
stack.toString(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace', // Makes code easier to read
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _copyToClipboard(context),
|
||||
label: const Text('Copy Details'),
|
||||
icon: const Icon(Icons.copy),
|
||||
backgroundColor: colorScheme.error,
|
||||
foregroundColor: colorScheme.onError,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionLabel(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyToClipboard(BuildContext context) {
|
||||
final text = '=== ERROR ===\n$error\n\n=== STACK TRACE ===\n$stack';
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Error details copied to clipboard'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export 'editor.dart';
|
||||
export 'error.dart';
|
||||
export 'home.dart';
|
||||
export 'scan.dart';
|
||||
export 'editor.dart';
|
||||
@@ -323,7 +323,6 @@ class NetworkDetection extends _$NetworkDetection
|
||||
with AutoDisposeNotifierMixin {
|
||||
bool? _preIsStart;
|
||||
CancelToken? _cancelToken;
|
||||
int _startMillisecondsEpoch = 0;
|
||||
|
||||
@override
|
||||
NetworkDetectionState build() {
|
||||
@@ -345,9 +344,6 @@ class NetworkDetection extends _$NetworkDetection
|
||||
if (!isStart && _preIsStart == false && state.ipInfo != null) {
|
||||
return;
|
||||
}
|
||||
final millisecondsEpoch = DateTime.now().millisecondsSinceEpoch;
|
||||
_startMillisecondsEpoch = millisecondsEpoch;
|
||||
final runTime = millisecondsEpoch + 1;
|
||||
_cancelToken?.cancel();
|
||||
_cancelToken = CancelToken();
|
||||
commonPrint.log('checkIp start');
|
||||
@@ -355,7 +351,7 @@ class NetworkDetection extends _$NetworkDetection
|
||||
_preIsStart = isStart;
|
||||
final res = await request.checkIp(cancelToken: _cancelToken);
|
||||
commonPrint.log('checkIp res: $res');
|
||||
if (res.isError && runTime > _startMillisecondsEpoch) {
|
||||
if (res.isError) {
|
||||
state = state.copyWith(isLoading: true, ipInfo: null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1822,7 +1822,7 @@ final class NetworkDetectionProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$networkDetectionHash() => r'501babec2bbf2a38e4fef96cf20c76e9352bc5ee';
|
||||
String _$networkDetectionHash() => r'29770de6a7ea2ac04b6584145d5200a7d43e45b0';
|
||||
|
||||
abstract class _$NetworkDetection extends $Notifier<NetworkDetectionState> {
|
||||
NetworkDetectionState build();
|
||||
|
||||
@@ -38,10 +38,10 @@ class GlobalState {
|
||||
late Measure measure;
|
||||
late CommonTheme theme;
|
||||
late Color accentColor;
|
||||
bool needInitStatus = true;
|
||||
CorePalette? corePalette;
|
||||
DateTime? startTime;
|
||||
UpdateTasks tasks = [];
|
||||
bool isUserDisconnected = false;
|
||||
SetupState? lastSetupState;
|
||||
VpnState? lastVpnState;
|
||||
|
||||
@@ -71,43 +71,51 @@ class GlobalState {
|
||||
}
|
||||
|
||||
Future<ProviderContainer> _initData(int version) async {
|
||||
final appState = AppState(
|
||||
brightness: WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
version: version,
|
||||
viewSize: Size.zero,
|
||||
requests: FixedList(maxLength),
|
||||
logs: FixedList(maxLength),
|
||||
traffics: FixedList(30),
|
||||
totalTraffic: Traffic(),
|
||||
systemUiOverlayStyle: const SystemUiOverlayStyle(),
|
||||
);
|
||||
final appStateOverrides = buildAppStateOverrides(appState);
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
final configMap = await preferences.getConfigMap();
|
||||
final config = await migration.migrationIfNeeded(
|
||||
configMap,
|
||||
sync: (data) async {
|
||||
final newConfigMap = data.configMap;
|
||||
final config = Config.realFromJson(newConfigMap);
|
||||
await Future.wait([
|
||||
database.restore(data.profiles, data.scripts, data.rules, data.links),
|
||||
preferences.saveConfig(config),
|
||||
]);
|
||||
return config;
|
||||
},
|
||||
);
|
||||
final configOverrides = buildConfigOverrides(config);
|
||||
final container = ProviderContainer(
|
||||
overrides: [...appStateOverrides, ...configOverrides],
|
||||
);
|
||||
final profiles = await database.profilesDao.all().get();
|
||||
container.read(profilesProvider.notifier).setAndReorder(profiles);
|
||||
await AppLocalizations.load(
|
||||
utils.getLocaleForString(config.appSettingProps.locale) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
await window?.init(version, config.windowProps);
|
||||
return container;
|
||||
try {
|
||||
final appState = AppState(
|
||||
brightness:
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
version: version,
|
||||
viewSize: Size.zero,
|
||||
requests: FixedList(maxLength),
|
||||
logs: FixedList(maxLength),
|
||||
traffics: FixedList(30),
|
||||
totalTraffic: Traffic(),
|
||||
systemUiOverlayStyle: const SystemUiOverlayStyle(),
|
||||
);
|
||||
final appStateOverrides = buildAppStateOverrides(appState);
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
final configMap = await preferences.getConfigMap();
|
||||
final migrationData = await migration.migrationIfNeeded(configMap);
|
||||
final newConfigMap = migrationData.configMap;
|
||||
|
||||
final config = newConfigMap != null
|
||||
? Config.fromJson(newConfigMap)
|
||||
: Config(themeProps: defaultThemeProps);
|
||||
final configOverrides = buildConfigOverrides(config);
|
||||
await database.restore(
|
||||
migrationData.profiles,
|
||||
migrationData.scripts,
|
||||
migrationData.rules,
|
||||
migrationData.links,
|
||||
);
|
||||
final container = ProviderContainer(
|
||||
overrides: [...appStateOverrides, ...configOverrides],
|
||||
);
|
||||
final profiles = await database.profilesDao.all().get();
|
||||
container.read(profilesProvider.notifier).setAndReorder(profiles);
|
||||
await AppLocalizations.load(
|
||||
utils.getLocaleForString(config.appSettingProps.locale) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
|
||||
await window?.init(version, config.windowProps);
|
||||
return container;
|
||||
} catch (e) {
|
||||
await migration.rollback();
|
||||
commonPrint.log('init failed $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startUpdateTasks([UpdateTasks? tasks]) async {
|
||||
|
||||
@@ -33,7 +33,10 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
parent: _controller!,
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
ref.listenManual(isStartProvider, (prev, next) {
|
||||
ref.listenManual(runTimeProvider.select((state) => state != null), (
|
||||
prev,
|
||||
next,
|
||||
) {
|
||||
if (next != isStart) {
|
||||
isStart = next;
|
||||
updateController();
|
||||
@@ -52,7 +55,7 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
debouncer.call(FunctionTag.updateStatus, () {
|
||||
appController.updateStatus(isStart, isInit: !ref.read(initProvider));
|
||||
appController.updateStatus(isStart);
|
||||
}, duration: commonDuration);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class _EditProfileViewState extends State<EditProfileView> {
|
||||
late final TextEditingController _labelController;
|
||||
late final TextEditingController _urlController;
|
||||
late final TextEditingController _autoUpdateDurationController;
|
||||
late bool _autoUpdate;
|
||||
late final bool _autoUpdate;
|
||||
String? _rawText;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final _fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:fl_clash/controller.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/features/overwrite/rule.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/providers/database.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -29,33 +28,10 @@ class _OverwriteViewState extends ConsumerState<OverwriteView> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _handlePreview() async {
|
||||
final profile = ref.read(profileProvider(widget.profileId));
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
final configMap = await appController.getProfileWithId(profile.id);
|
||||
final content = await encodeYamlTask(configMap);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final previewPage = EditorPage(title: profile.realLabel, content: content);
|
||||
BaseNavigator.push<String>(context, previewPage);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonScaffold(
|
||||
title: appLocalizations.override,
|
||||
actions: [
|
||||
CommonMinFilledButtonTheme(
|
||||
child: FilledButton(
|
||||
onPressed: _handlePreview,
|
||||
child: Text(appLocalizations.preview),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
],
|
||||
body: CustomScrollView(
|
||||
slivers: [_Title(widget.profileId), _Content(widget.profileId)],
|
||||
),
|
||||
@@ -365,9 +341,7 @@ class _ScriptContent extends ConsumerWidget {
|
||||
|
||||
void _handleChange(WidgetRef ref, int scriptId) {
|
||||
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
|
||||
return state.copyWith(
|
||||
scriptId: state.scriptId == scriptId ? null : scriptId,
|
||||
);
|
||||
return state.copyWith(scriptId: scriptId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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.92+2026012504
|
||||
version: 0.8.92+2026012301
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
|
||||
25
setup.dart
25
setup.dart
@@ -369,15 +369,6 @@ class BuildCommand extends Command {
|
||||
.map((e) => e.arch!)
|
||||
.toList();
|
||||
|
||||
Future<void> _buildEnvFile(String env, {String? coreSha256}) async {
|
||||
final data = {
|
||||
'APP_ENV': env,
|
||||
if (coreSha256 != null) 'CORE_SHA256': coreSha256,
|
||||
};
|
||||
final envFile = File(join(current, 'env.json'))..create();
|
||||
await envFile.writeAsString(json.encode(data));
|
||||
}
|
||||
|
||||
Future<void> _getLinuxDependencies(Arch arch) async {
|
||||
await Build.exec(Build.getExecutable('sudo apt update -y'));
|
||||
await Build.exec(
|
||||
@@ -421,7 +412,7 @@ class BuildCommand extends Command {
|
||||
await Build.exec(
|
||||
name: name,
|
||||
Build.getExecutable(
|
||||
'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose,dart-define-from-file=env.json$args',
|
||||
'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose$args --build-dart-define=APP_ENV=$env',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -457,23 +448,21 @@ class BuildCommand extends Command {
|
||||
mode: mode,
|
||||
);
|
||||
|
||||
String? coreSha256;
|
||||
|
||||
if (Platform.isWindows) {
|
||||
coreSha256 = await Build.calcSha256(corePaths.first);
|
||||
await Build.buildHelper(target, coreSha256);
|
||||
}
|
||||
await _buildEnvFile(env, coreSha256: coreSha256);
|
||||
if (out != 'app') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
case Target.windows:
|
||||
final token = target != Target.android
|
||||
? await Build.calcSha256(corePaths.first)
|
||||
: null;
|
||||
Build.buildHelper(target, token!);
|
||||
_buildDistributor(
|
||||
target: target,
|
||||
targets: 'exe,zip',
|
||||
args: ' --description $archName',
|
||||
args:
|
||||
' --description $archName --build-dart-define=CORE_SHA256=$token',
|
||||
env: env,
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user