Compare commits

..

1 Commits

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

Optimize backup and restore

Optimize more details
2026-01-23 13:49:18 +08:00
28 changed files with 183 additions and 416 deletions

View File

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

View File

@@ -64,7 +64,7 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".dev"
applicationIdSuffix = ".debug"
}
release {

View File

@@ -181,18 +181,20 @@ object State {
}
}
suspend fun handleStopService() {
runLock.withLock {
if (runStateFlow.value != RunState.START) {
return
}
try {
runStateFlow.tryEmit(RunState.PENDING)
runTime = Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.START)
fun handleStopService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value != RunState.START) {
return@launch
}
try {
runStateFlow.tryEmit(RunState.PENDING)
runTime = Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.START)
}
}
}
}

View File

@@ -86,10 +86,8 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
private fun handleStop(result: MethodChannel.Result) {
launch {
State.handleStopService()
result.success(true)
}
State.handleStopService()
result.success(true)
}
val semaphore = Semaphore(10)

View File

@@ -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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
@@ -550,20 +553,18 @@ extension SetupControllerExt on AppController {
Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
if (isStart) {
_ref.read(runTimeProvider.notifier).update((state) {
return state ?? 0;
});
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;
await applyProfile(
force: true,
preloadInvoke: () async {
@@ -611,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);
@@ -808,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();
@@ -827,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)) {
@@ -1188,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 {

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

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,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);
}
}

View File

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

View File

@@ -1,120 +0,0 @@
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.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colorScheme.error.withOpacity(0.5)),
),
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.withOpacity(0.5)),
),
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),
),
);
}
}

View File

@@ -1,4 +1,3 @@
export 'editor.dart';
export 'error.dart';
export 'home.dart';
export 'scan.dart';
export 'editor.dart';

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ class GlobalState {
CorePalette? corePalette;
DateTime? startTime;
UpdateTasks tasks = [];
bool isUserDisconnected = false;
SetupState? lastSetupState;
VpnState? lastVpnState;
@@ -70,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 {

View File

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

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+2026012501
version: 0.8.92+2026012301
environment:
sdk: '>=3.8.0 <4.0.0'

View File

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