Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
dbd8fd89fe Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-01-26 01:31:32 +08:00
22 changed files with 185 additions and 92 deletions

7
.run/main.dart.run.xml Normal file
View File

@@ -0,0 +1,7 @@
<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>

View File

@@ -1,10 +0,0 @@
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

View File

@@ -99,27 +99,47 @@ func handleValidateConfig(path string) string {
func handleGetProxies() ProxiesData {
runLock.Lock()
defer runLock.Unlock()
var all = []string{"GLOBAL"}
all = append(all, config.ProxyList...)
nameList := config.GetProxyNameList()
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() {
name := proxy.Name()
proxies[name] = proxy
proxies[proxy.Name()] = proxy
}
}
types := []constant.AdapterType{
constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance,
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:
}
}
nextAll := slices.DeleteFunc(all, func(name string) bool {
return !slices.Contains(types, proxies[name].Type())
})
if !hasGlobal {
if p, ok := proxies["GLOBAL"]; ok && p != nil {
allNames = append([]string{"GLOBAL"}, allNames...)
}
}
return ProxiesData{
All: nextAll,
All: allNames,
Proxies: proxies,
}
}

View File

@@ -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.fileName),
_getProfilePath(homeDirPath, item.fileName),
_getProfilePath(restoreDirPath, item.id.toString()),
_getProfilePath(homeDirPath, item.id.toString()),
),
);
final scriptsMigration = scripts.map(
(item) => VM2(
_getScriptPath(restoreDirPath, item.fileName),
_getScriptPath(homeDirPath, item.fileName),
_getScriptPath(restoreDirPath, item.id.toString()),
_getScriptPath(homeDirPath, item.id.toString()),
),
);
await _copyWithMapList([...profilesMigration, ...scriptsMigration]);

View File

@@ -26,6 +26,10 @@ 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';

View File

@@ -83,6 +83,7 @@ class Window {
}
Future<void> close() async {
await windowManager.close();
exit(0);
}

View File

@@ -7,7 +7,6 @@ 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';
@@ -139,6 +138,11 @@ 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();
}
@@ -551,18 +555,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 {
_ref.read(runTimeProvider.notifier).value = 0;
globalState.needInitStatus = false;
await applyProfile(
force: true,
preloadInvoke: () async {
@@ -610,6 +614,18 @@ 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);
@@ -716,6 +732,7 @@ 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) {
@@ -795,9 +812,6 @@ 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();
@@ -817,8 +831,8 @@ extension CoreControllerExt on AppController {
}
Future<void> restartCore([bool start = false]) async {
globalState.isUserDisconnected = true;
await coreController.shutdown();
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
await coreController.shutdown(true);
await _connectCore();
await _initCore();
if (start || _ref.read(isStartProvider)) {
@@ -832,7 +846,7 @@ extension CoreControllerExt on AppController {
if (coreController.isCompleted) {
return false;
}
await restartCore();
await restartCore(start);
return true;
}
@@ -858,12 +872,13 @@ extension SystemControllerExt on AppController {
system.exit();
});
try {
if (needSave) {
await preferences.saveConfig(config);
}
await proxy?.stopProxy();
await macOS?.updateDns(true);
await coreController.destroy();
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(),
]);
commonPrint.log('exit');
} finally {
system.exit();
@@ -1178,8 +1193,8 @@ extension CommonControllerExt on AppController {
}
final res = await futureFunction();
return res;
} catch (e) {
commonPrint.log('$title===> $e', logLevel: LogLevel.warning);
} catch (e, s) {
commonPrint.log('$title ===> $e, $s', logLevel: LogLevel.warning);
if (silence) {
globalState.showNotifier(e.toString());
} else {

View File

@@ -65,8 +65,8 @@ class CoreController {
);
}
Future<void> shutdown() async {
await _interface.shutdown();
Future<void> shutdown(bool isUser) async {
await _interface.shutdown(isUser);
}
FutureOr<bool> get isInit => _interface.isInit;
@@ -201,9 +201,10 @@ class CoreController {
final profilePath = await appPath.getProfilePath(id.toString());
final res = await _interface.getConfig(profilePath);
if (res.isSuccess) {
res.data['rules'] = res.data['rule'];
res.data.remove('rule');
return res.data;
final data = Map<String, dynamic>.from(res.data);
data['rules'] = data['rule'];
data.remove('rule');
return data;
} else {
throw res.message;
}

View File

@@ -11,7 +11,7 @@ mixin CoreInterface {
Future<String> preload();
Future<bool> shutdown();
Future<bool> shutdown(bool isUser);
Future<bool> get isInit;
@@ -86,7 +86,9 @@ abstract class CoreHandlerInterface with CoreInterface {
Duration? timeout,
}) async {
try {
await completer.future.timeout(const Duration(seconds: 10));
if (!completer.isCompleted) {
return null;
}
} catch (e) {
commonPrint.log(
'Invoke pre ${method.name} timeout $e',
@@ -98,9 +100,9 @@ abstract class CoreHandlerInterface with CoreInterface {
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
}
return utils.handleWatch(
return await utils.handleWatch(
function: () async {
return await invoke(method: method, data: data, timeout: timeout);
return await invoke<T>(method: method, data: data, timeout: timeout);
},
onWatch: (data, elapsedMilliseconds) {
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
@@ -131,7 +133,7 @@ abstract class CoreHandlerInterface with CoreInterface {
}
@override
Future<bool> shutdown();
Future<bool> shutdown(bool isUser);
@override
Future<bool> get isInit async {
@@ -163,8 +165,8 @@ abstract class CoreHandlerInterface with CoreInterface {
@override
Future<Result> getConfig(String path) async {
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ??
Result.success({});
final res = await _invoke(method: ActionMethod.getConfig, data: path);
return res ?? Result.success({});
}
@override

View File

@@ -37,10 +37,12 @@ class CoreLib extends CoreHandlerInterface {
}
@override
Future<bool> shutdown() async {
await service?.shutdown();
Future<bool> shutdown(_) async {
if (!_connectedCompleter.isCompleted) {
return false;
}
_connectedCompleter = Completer();
return true;
return service?.shutdown() ?? true;
}
@override

View File

@@ -16,6 +16,8 @@ class CoreService extends CoreHandlerInterface {
Completer<Socket> _socketCompleter = Completer();
Completer<bool> _shutdownCompleter = Completer();
final Map<String, Completer> _callbackCompleterMap = {};
Process? _process;
@@ -35,6 +37,9 @@ class CoreService extends CoreHandlerInterface {
if (result.id?.isEmpty == true) {
coreEventManager.sendEvent(CoreEvent.fromJson(result.data));
}
if (completer?.isCompleted == true) {
return;
}
completer?.complete(data);
}
@@ -76,6 +81,9 @@ class CoreService extends CoreHandlerInterface {
})
.onDone(() {
_handleInvokeCrashEvent();
if (!_shutdownCompleter.isCompleted) {
_shutdownCompleter.complete(true);
}
});
}
@@ -87,7 +95,7 @@ class CoreService extends CoreHandlerInterface {
Future<void> start() async {
if (_process != null) {
await shutdown();
await shutdown(false);
}
final serverSocket = await _serverCompleter.future;
final arg = system.isWindows
@@ -113,7 +121,7 @@ class CoreService extends CoreHandlerInterface {
@override
destroy() async {
final server = await _serverCompleter.future;
await shutdown();
await shutdown(false);
await server.close();
await _deleteSocketFile();
return true;
@@ -135,12 +143,16 @@ class CoreService extends CoreHandlerInterface {
if (_socketCompleter.isCompleted) {
final socket = await _socketCompleter.future;
_socketCompleter = Completer();
socket.close();
await socket.close();
}
}
@override
shutdown() async {
shutdown(bool isUser) async {
if (!_socketCompleter.isCompleted && _process == null) {
return false;
}
_shutdownCompleter = Completer();
await _destroySocket();
_clearCompleter();
if (system.isWindows) {
@@ -148,7 +160,11 @@ class CoreService extends CoreHandlerInterface {
}
_process?.kill();
_process = null;
return true;
if (isUser) {
return _shutdownCompleter.future;
} else {
return true;
}
}
void _clearCompleter() {

View File

@@ -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();
}
});

View File

@@ -98,16 +98,14 @@ 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;
await coreController.shutdown();
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
context.showNotifier(message);
}
await coreController.shutdown(false);
super.onCrash(message);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart';
@@ -125,7 +127,7 @@ class _EditorPageState extends ConsumerState<EditorPage> {
if (file == null) {
return;
}
final res = String.fromCharCodes(file.bytes?.toList() ?? []);
final res = utf8.decode(file.bytes?.toList() ?? []);
_controller.text = res;
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/common/color.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -49,9 +50,9 @@ class InitErrorScreen extends StatelessWidget {
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.5),
color: colorScheme.errorContainer.opacity50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colorScheme.error.withOpacity(0.5)),
border: Border.all(color: colorScheme.error.opacity50),
),
child: SelectableText(
error.toString(),
@@ -71,7 +72,7 @@ class InitErrorScreen extends StatelessWidget {
? Colors.grey[900]
: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.withOpacity(0.5)),
border: Border.all(color: Colors.grey.opacity50),
),
child: SelectableText(
stack.toString(),

View File

@@ -1822,7 +1822,7 @@ final class NetworkDetectionProvider
}
}
String _$networkDetectionHash() => r'29770de6a7ea2ac04b6584145d5200a7d43e45b0';
String _$networkDetectionHash() => r'501babec2bbf2a38e4fef96cf20c76e9352bc5ee';
abstract class _$NetworkDetection extends $Notifier<NetworkDetectionState> {
NetworkDetectionState build();

View File

@@ -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;

View File

@@ -33,10 +33,7 @@ class _StartButtonState extends ConsumerState<StartButton>
parent: _controller!,
curve: Curves.easeOutBack,
);
ref.listenManual(runTimeProvider.select((state) => state != null), (
prev,
next,
) {
ref.listenManual(isStartProvider, (prev, next) {
if (next != isStart) {
isStart = next;
updateController();
@@ -55,7 +52,7 @@ class _StartButtonState extends ConsumerState<StartButton>
isStart = !isStart;
updateController();
debouncer.call(FunctionTag.updateStatus, () {
appController.updateStatus(isStart);
appController.updateStatus(isStart, isInit: !ref.read(initProvider));
}, duration: commonDuration);
}

View File

@@ -5,6 +5,7 @@ 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';
@@ -28,10 +29,33 @@ 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)],
),
@@ -341,7 +365,9 @@ class _ScriptContent extends ConsumerWidget {
void _handleChange(WidgetRef ref, int scriptId) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(scriptId: scriptId);
return state.copyWith(
scriptId: state.scriptId == scriptId ? null : scriptId,
);
});
}

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.8.92+2026012403
version: 0.8.92+2026012504
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -369,6 +369,15 @@ 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(
@@ -412,7 +421,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$args --build-dart-define=APP_ENV=$env',
'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose,dart-define-from-file=env.json$args',
),
);
}
@@ -448,21 +457,23 @@ 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 --build-dart-define=CORE_SHA256=$token',
args: ' --description $archName',
env: env,
);
return;