Compare commits

..

3 Commits

Author SHA1 Message Date
chen08209
672eaccd35 Update changelog 2026-02-02 02:38:00 +00:00
chen08209
2fbb96f5c1 Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-02-02 10:15:42 +08:00
chen08209
243b3037d9 Update changelog 2025-12-12 06:52:14 +00:00
49 changed files with 654 additions and 344 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,3 +1,23 @@
## v0.8.92
- Add sqlite store
- Optimize android quick action
- Optimize backup and restore
- Optimize more details
## v0.8.91
- Fix windows some issues
- Optimize overwrite handle
- Optimize access control page
- Optimize some details
## v0.8.90 ## v0.8.90
- Fix android tile service - Fix android tile service

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

@@ -475,5 +475,6 @@
"restoreFromFileDesc": "Restore data via file", "restoreFromFileDesc": "Restore data via file",
"restoreOnlyConfig": "Restore configuration files only", "restoreOnlyConfig": "Restore configuration files only",
"restoreAllData": "Restore all data", "restoreAllData": "Restore all data",
"addProfile": "Add Profile" "addProfile": "Add Profile",
"delayTest": "Delay Test"
} }

View File

@@ -476,5 +476,6 @@
"restoreFromFileDesc": "ファイルを介してデータを復元する", "restoreFromFileDesc": "ファイルを介してデータを復元する",
"restoreOnlyConfig": "設定ファイルのみを復元する", "restoreOnlyConfig": "設定ファイルのみを復元する",
"restoreAllData": "すべてのデータを復元する", "restoreAllData": "すべてのデータを復元する",
"addProfile": "プロファイルを追加" "addProfile": "プロファイルを追加",
"delayTest": "遅延テスト"
} }

View File

@@ -484,5 +484,6 @@
"restoreFromFileDesc": "Восстановить данные из файла", "restoreFromFileDesc": "Восстановить данные из файла",
"restoreOnlyConfig": "Восстановить только файлы конфигурации", "restoreOnlyConfig": "Восстановить только файлы конфигурации",
"restoreAllData": "Восстановить все данные", "restoreAllData": "Восстановить все данные",
"addProfile": "Добавить профиль" "addProfile": "Добавить профиль",
"delayTest": "Тест задержки"
} }

View File

@@ -476,5 +476,6 @@
"restoreFromFileDesc": "通过文件恢复数据", "restoreFromFileDesc": "通过文件恢复数据",
"restoreOnlyConfig": "仅恢复配置文件", "restoreOnlyConfig": "仅恢复配置文件",
"restoreAllData": "恢复所有数据", "restoreAllData": "恢复所有数据",
"addProfile": "添加配置" "addProfile": "添加配置",
"delayTest": "延迟测试"
} }

View File

@@ -99,27 +99,47 @@ func handleValidateConfig(path string) string {
func handleGetProxies() ProxiesData { func handleGetProxies() ProxiesData {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
var all = []string{"GLOBAL"}
all = append(all, config.ProxyList...) nameList := config.GetProxyNameList()
proxies := make(map[string]constant.Proxy) proxies := make(map[string]constant.Proxy)
for name, proxy := range tunnel.Proxies() { for name, proxy := range tunnel.Proxies() {
proxies[name] = proxy proxies[name] = proxy
} }
for _, p := range tunnel.Providers() { for _, p := range tunnel.Providers() {
for _, proxy := range p.Proxies() { for _, proxy := range p.Proxies() {
name := proxy.Name() proxies[proxy.Name()] = proxy
proxies[name] = proxy
} }
} }
types := []constant.AdapterType{ hasGlobal := false
constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance, 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{ return ProxiesData{
All: nextAll, All: allNames,
Proxies: proxies, Proxies: proxies,
} }
} }
@@ -512,6 +532,9 @@ func handleDelFile(path string, result ActionResult) {
} }
func handleSetupConfig(bytes []byte) string { func handleSetupConfig(bytes []byte) string {
if !isInit {
return "not initialized"
}
var params = defaultSetupParams() var params = defaultSetupParams()
err := UnmarshalJson(bytes, params) err := UnmarshalJson(bytes, params)
if err != nil { if err != nil {

View File

@@ -20,14 +20,14 @@ const helperPort = 47890;
const maxTextScale = 1.4; const maxTextScale = 1.4;
const minTextScale = 0.8; const minTextScale = 0.8;
final baseInfoEdgeInsets = EdgeInsets.symmetric( final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16.ap, vertical: 16.mAp,
horizontal: 16.ap, horizontal: 16.mAp,
); );
final listHeaderPadding = EdgeInsets.only( final listHeaderPadding = EdgeInsets.only(
left: 16.ap, left: 16.mAp,
right: 8.ap, right: 8.mAp,
top: 24.ap, top: 24.mAp,
bottom: 8.ap, bottom: 8.mAp,
); );
const watchExecution = true; const watchExecution = true;
@@ -102,7 +102,8 @@ const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3; const defaultPrimaryColor = 0XFFD8C0C3;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 80 + (lines - 1) * 16, 0).ap; final space = 14.mAp;
return max(lines * (80.ap + space) - space, 0);
} }
const maxLength = 1000; const maxLength = 1000;

View File

@@ -19,10 +19,19 @@ class Migration {
required Future<Config> Function(MigrationData data) sync, required Future<Config> Function(MigrationData data) sync,
}) async { }) async {
_oldVersion = await preferences.getVersion(); _oldVersion = await preferences.getVersion();
MigrationData data = MigrationData(configMap: configMap);
if (_oldVersion == currentVersion) { if (_oldVersion == currentVersion) {
return Config.realFromJson(configMap); 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) { if (_oldVersion == 0 && configMap != null) {
final clashConfigMap = await preferences.getClashConfigMap(); final clashConfigMap = await preferences.getClashConfigMap();
if (clashConfigMap != null) { if (clashConfigMap != null) {

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -20,6 +22,10 @@ extension NumExt on num {
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5); return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
} }
double get mAp {
return this * min((1 + (globalState.theme.textScaleFactor - 1) * 0.5), 1);
}
TrafficShow get traffic { TrafficShow get traffic {
final units = TrafficUnit.values; final units = TrafficUnit.values;
var size = toDouble(); var size = toDouble();
@@ -51,7 +57,7 @@ extension NumExt on num {
extension DoubleExt on double { extension DoubleExt on double {
bool moreOrEqual(double value) { bool moreOrEqual(double value) {
return this > value || (value - this).abs() < precisionErrorTolerance + 2; return this > value || (value - this).abs() < precisionErrorTolerance + 1;
} }
} }

View File

@@ -576,14 +576,14 @@ Future<MigrationData> _restoreTask(RootIsolateToken token) async {
final scripts = results[1].cast<Script>(); final scripts = results[1].cast<Script>();
final profilesMigration = profiles.map( final profilesMigration = profiles.map(
(item) => VM2( (item) => VM2(
_getProfilePath(restoreDirPath, item.fileName), _getProfilePath(restoreDirPath, item.id.toString()),
_getProfilePath(homeDirPath, item.fileName), _getProfilePath(homeDirPath, item.id.toString()),
), ),
); );
final scriptsMigration = scripts.map( final scriptsMigration = scripts.map(
(item) => VM2( (item) => VM2(
_getScriptPath(restoreDirPath, item.fileName), _getScriptPath(restoreDirPath, item.id.toString()),
_getScriptPath(homeDirPath, item.fileName), _getScriptPath(homeDirPath, item.id.toString()),
), ),
); );
await _copyWithMapList([...profilesMigration, ...scriptsMigration]); await _copyWithMapList([...profilesMigration, ...scriptsMigration]);

View File

@@ -26,6 +26,10 @@ class Tray {
return system.isWindows ? 'ico' : 'png'; return system.isWindows ? 'ico' : 'png';
} }
Future<void> destroy() async {
await trayManager.destroy();
}
String getTryIcon({required bool isStart, required bool tunEnable}) { String getTryIcon({required bool isStart, required bool tunEnable}) {
if (system.isMacOS || !isStart) { if (system.isMacOS || !isStart) {
return 'assets/images/icon/status_1.$trayIconSuffix'; return 'assets/images/icon/status_1.$trayIconSuffix';

View File

@@ -83,6 +83,7 @@ class Window {
} }
Future<void> close() async { Future<void> close() async {
await windowManager.close();
exit(0); 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/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -41,26 +40,28 @@ class AppController {
extension InitControllerExt on AppController { extension InitControllerExt on AppController {
Future<void> _init() async { Future<void> _init() async {
try { FlutterError.onError = (details) {
updateTray(); commonPrint.log(
autoUpdateProfiles(); 'exception: ${details.exception} stack: ${details.stack}',
autoCheckUpdate(); logLevel: LogLevel.warning,
autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch); );
if (!_ref.read(appSettingProvider).silentLaunch) { };
window?.show(); updateTray();
} else { autoUpdateProfiles();
window?.hide(); autoCheckUpdate();
} autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch);
await _handleFailedPreference(); if (!_ref.read(appSettingProvider).silentLaunch) {
await _handlerDisclaimer(); window?.show();
await _showCrashlyticsTip(); } else {
await _connectCore(); window?.hide();
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 { Future<void> _handleFailedPreference() async {
@@ -137,6 +138,11 @@ extension InitControllerExt on AppController {
} }
Future<void> _initStatus() async { Future<void> _initStatus() async {
if (!globalState.needInitStatus) {
commonPrint.log('init status cancel');
return;
}
commonPrint.log('init status');
if (system.isAndroid) { if (system.isAndroid) {
await globalState.updateStartTime(); await globalState.updateStartTime();
} }
@@ -549,18 +555,18 @@ extension SetupControllerExt on AppController {
Future<void> updateStatus(bool isStart, {bool isInit = false}) async { Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
if (isStart) { if (isStart) {
final res = await tryStartCore(true);
if (res) {
return;
}
if (!isInit) { if (!isInit) {
final res = await tryStartCore(true);
if (res) {
return;
}
if (!_ref.read(initProvider)) { if (!_ref.read(initProvider)) {
return; return;
} }
await globalState.handleStart([updateRunTime, updateTraffic]); await globalState.handleStart([updateRunTime, updateTraffic]);
applyProfileDebounce(force: true, silence: true); applyProfileDebounce(force: true, silence: true);
} else { } else {
_ref.read(runTimeProvider.notifier).value = 0; globalState.needInitStatus = false;
await applyProfile( await applyProfile(
force: true, force: true,
preloadInvoke: () async { preloadInvoke: () async {
@@ -608,6 +614,18 @@ extension SetupControllerExt on AppController {
_ref.read(checkIpNumProvider.notifier).add(); _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}) { void applyProfileDebounce({bool silence = false, bool force = false}) {
debouncer.call(FunctionTag.applyProfile, (silence, force) { debouncer.call(FunctionTag.applyProfile, (silence, force) {
applyProfile(silence: silence, force: force); applyProfile(silence: silence, force: force);
@@ -635,9 +653,14 @@ extension SetupControllerExt on AppController {
bool force = false, bool force = false,
VoidCallback? preloadInvoke, VoidCallback? preloadInvoke,
}) async { }) async {
if (!force && !await needSetup()) {
return;
}
await loadingRun( await loadingRun(
() async { () async {
await _applyProfile(force, preloadInvoke); await _setupConfig(preloadInvoke);
await updateGroups();
await updateProviders();
}, },
silence: true, silence: true,
tag: !silence ? LoadingTag.proxies : null, tag: !silence ? LoadingTag.proxies : null,
@@ -707,13 +730,8 @@ extension SetupControllerExt on AppController {
return res; return res;
} }
Future<void> _setupConfig([ Future<void> _setupConfig([VoidCallback? preloadInvoke]) async {
bool force = false, commonPrint.log('setup ===>');
VoidCallback? preloadInvoke,
]) async {
if (!force && !await needSetup()) {
return;
}
var profile = _ref.read(currentProfileProvider); var profile = _ref.read(currentProfileProvider);
final nextProfile = await profile?.checkAndUpdateAndCopy(); final nextProfile = await profile?.checkAndUpdateAndCopy();
if (nextProfile != null) { if (nextProfile != null) {
@@ -731,9 +749,6 @@ extension SetupControllerExt on AppController {
globalState.lastSetupState = setupState; globalState.lastSetupState = setupState;
if (system.isAndroid) { if (system.isAndroid) {
globalState.lastVpnState = _ref.read(vpnStateProvider); globalState.lastVpnState = _ref.read(vpnStateProvider);
}
if (system.isAndroid) {
preferences.saveShareState(this.sharedState); preferences.saveShareState(this.sharedState);
} }
final config = await getProfile( final config = await getProfile(
@@ -753,15 +768,6 @@ extension SetupControllerExt on AppController {
} }
addCheckIp(); addCheckIp();
} }
Future _applyProfile([
bool force = false,
VoidCallback? preloadInvoke,
]) async {
await _setupConfig(force, preloadInvoke);
await updateGroups();
await updateProviders();
}
} }
extension CoreControllerExt on AppController { extension CoreControllerExt on AppController {
@@ -793,9 +799,6 @@ extension CoreControllerExt on AppController {
} }
Future<Result<bool>> _requestAdmin(bool enableTun) async { Future<Result<bool>> _requestAdmin(bool enableTun) async {
if (system.isWindows && kDebugMode) {
return Result.success(false);
}
final realTunEnable = _ref.read(realTunEnableProvider); final realTunEnable = _ref.read(realTunEnableProvider);
if (enableTun != realTunEnable && realTunEnable == false) { if (enableTun != realTunEnable && realTunEnable == false) {
final code = await system.authorizeCore(); final code = await system.authorizeCore();
@@ -815,8 +818,8 @@ extension CoreControllerExt on AppController {
} }
Future<void> restartCore([bool start = false]) async { Future<void> restartCore([bool start = false]) async {
globalState.isUserDisconnected = true; _ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
await coreController.shutdown(); await coreController.shutdown(true);
await _connectCore(); await _connectCore();
await _initCore(); await _initCore();
if (start || _ref.read(isStartProvider)) { if (start || _ref.read(isStartProvider)) {
@@ -830,7 +833,7 @@ extension CoreControllerExt on AppController {
if (coreController.isCompleted) { if (coreController.isCompleted) {
return false; return false;
} }
await restartCore(); await restartCore(start);
return true; return true;
} }
@@ -856,11 +859,12 @@ extension SystemControllerExt on AppController {
system.exit(); system.exit();
}); });
try { try {
if (needSave) { await Future.wait([
await preferences.saveConfig(config); if (needSave) preferences.saveConfig(config),
} if (macOS != null) macOS!.updateDns(true),
await proxy?.stopProxy(); if (proxy != null) proxy!.stopProxy(),
await macOS?.updateDns(true); if (tray != null) tray!.destroy(),
]);
await coreController.destroy(); await coreController.destroy();
commonPrint.log('exit'); commonPrint.log('exit');
} finally { } finally {
@@ -1176,8 +1180,8 @@ extension CommonControllerExt on AppController {
} }
final res = await futureFunction(); final res = await futureFunction();
return res; return res;
} catch (e) { } catch (e, s) {
commonPrint.log('$title===> $e', logLevel: LogLevel.warning); commonPrint.log('$title ===> $e, $s', logLevel: LogLevel.warning);
if (silence) { if (silence) {
globalState.showNotifier(e.toString()); globalState.showNotifier(e.toString());
} else { } else {

View File

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

View File

@@ -11,7 +11,7 @@ mixin CoreInterface {
Future<String> preload(); Future<String> preload();
Future<bool> shutdown(); Future<bool> shutdown(bool isUser);
Future<bool> get isInit; Future<bool> get isInit;
@@ -98,9 +98,9 @@ abstract class CoreHandlerInterface with CoreInterface {
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data'); commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
} }
return utils.handleWatch( return await utils.handleWatch(
function: () async { function: () async {
return await invoke(method: method, data: data, timeout: timeout); return await invoke<T>(method: method, data: data, timeout: timeout);
}, },
onWatch: (data, elapsedMilliseconds) { onWatch: (data, elapsedMilliseconds) {
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms'); commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
@@ -131,7 +131,7 @@ abstract class CoreHandlerInterface with CoreInterface {
} }
@override @override
Future<bool> shutdown(); Future<bool> shutdown(bool isUser);
@override @override
Future<bool> get isInit async { Future<bool> get isInit async {
@@ -163,8 +163,8 @@ abstract class CoreHandlerInterface with CoreInterface {
@override @override
Future<Result> getConfig(String path) async { Future<Result> getConfig(String path) async {
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ?? final res = await _invoke(method: ActionMethod.getConfig, data: path);
Result.success({}); return res ?? Result.success({});
} }
@override @override

View File

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

View File

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

View File

@@ -270,6 +270,7 @@ class MessageLookup extends MessageLookupByLibrary {
"defaultText": MessageLookupByLibrary.simpleMessage("Default"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"),
"delay": MessageLookupByLibrary.simpleMessage("Delay"), "delay": MessageLookupByLibrary.simpleMessage("Delay"),
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
"delayTest": MessageLookupByLibrary.simpleMessage("Delay Test"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"), "delete": MessageLookupByLibrary.simpleMessage("Delete"),
"deleteMultipTip": m1, "deleteMultipTip": m1,
"deleteTip": m2, "deleteTip": m2,

View File

@@ -205,6 +205,7 @@ class MessageLookup extends MessageLookupByLibrary {
"defaultText": MessageLookupByLibrary.simpleMessage("デフォルト"), "defaultText": MessageLookupByLibrary.simpleMessage("デフォルト"),
"delay": MessageLookupByLibrary.simpleMessage("遅延"), "delay": MessageLookupByLibrary.simpleMessage("遅延"),
"delaySort": MessageLookupByLibrary.simpleMessage("遅延順"), "delaySort": MessageLookupByLibrary.simpleMessage("遅延順"),
"delayTest": MessageLookupByLibrary.simpleMessage("遅延テスト"),
"delete": MessageLookupByLibrary.simpleMessage("削除"), "delete": MessageLookupByLibrary.simpleMessage("削除"),
"deleteMultipTip": m1, "deleteMultipTip": m1,
"deleteTip": m2, "deleteTip": m2,

View File

@@ -277,6 +277,7 @@ class MessageLookup extends MessageLookupByLibrary {
"defaultText": MessageLookupByLibrary.simpleMessage("По умолчанию"), "defaultText": MessageLookupByLibrary.simpleMessage("По умолчанию"),
"delay": MessageLookupByLibrary.simpleMessage("Задержка"), "delay": MessageLookupByLibrary.simpleMessage("Задержка"),
"delaySort": MessageLookupByLibrary.simpleMessage("Сортировка по задержке"), "delaySort": MessageLookupByLibrary.simpleMessage("Сортировка по задержке"),
"delayTest": MessageLookupByLibrary.simpleMessage("Тест задержки"),
"delete": MessageLookupByLibrary.simpleMessage("Удалить"), "delete": MessageLookupByLibrary.simpleMessage("Удалить"),
"deleteMultipTip": m1, "deleteMultipTip": m1,
"deleteTip": m2, "deleteTip": m2,

View File

@@ -185,6 +185,7 @@ class MessageLookup extends MessageLookupByLibrary {
"defaultText": MessageLookupByLibrary.simpleMessage("默认"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"), "delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delayTest": MessageLookupByLibrary.simpleMessage("延迟测试"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteMultipTip": m1, "deleteMultipTip": m1,
"deleteTip": m2, "deleteTip": m2,

View File

@@ -3743,6 +3743,11 @@ class AppLocalizations {
String get addProfile { String get addProfile {
return Intl.message('Add Profile', name: 'addProfile', desc: '', args: []); return Intl.message('Add Profile', name: 'addProfile', desc: '', args: []);
} }
/// `Delay Test`
String get delayTest {
return Intl.message('Delay Test', name: 'delayTest', desc: '', args: []);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/pages/error.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -9,11 +10,22 @@ import 'application.dart';
import 'common/common.dart'; import 'common/common.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); try {
final version = await system.version; WidgetsFlutterBinding.ensureInitialized();
final container = await globalState.init(version); final version = await system.version;
HttpOverrides.global = FlClashHttpOverrides(); final container = await globalState.init(version);
runApp( HttpOverrides.global = FlClashHttpOverrides();
UncontrolledProviderScope(container: container, child: const Application()), runApp(
); UncontrolledProviderScope(
container: container,
child: const Application(),
),
);
} catch (e, s) {
return runApp(
MaterialApp(
home: InitErrorScreen(error: e, stack: s),
),
);
}
} }

View File

@@ -68,8 +68,8 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
render?.resume(); render?.resume();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appController.tryCheckIp();
if (system.isAndroid) { if (system.isAndroid) {
appController.addCheckIp();
appController.tryStartCore(); appController.tryStartCore();
} }
}); });

View File

@@ -98,16 +98,14 @@ class _CoreContainerState extends ConsumerState<CoreManager>
@override @override
Future<void> onCrash(String message) async { 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) { if (ref.read(coreStatusProvider) != CoreStatus.connected) {
return; return;
} }
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected; 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); super.onCrash(message);
} }
} }

View File

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

121
lib/pages/error.dart Normal file
View File

@@ -0,0 +1,121 @@
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),
),
);
}
}

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'database/database.dart'; import 'database/database.dart';
import 'enum/enum.dart';
import 'l10n/l10n.dart'; import 'l10n/l10n.dart';
import 'models/models.dart'; import 'models/models.dart';
@@ -39,10 +38,10 @@ class GlobalState {
late Measure measure; late Measure measure;
late CommonTheme theme; late CommonTheme theme;
late Color accentColor; late Color accentColor;
bool needInitStatus = true;
CorePalette? corePalette; CorePalette? corePalette;
DateTime? startTime; DateTime? startTime;
UpdateTasks tasks = []; UpdateTasks tasks = [];
bool isUserDisconnected = false;
SetupState? lastSetupState; SetupState? lastSetupState;
VpnState? lastVpnState; VpnState? lastVpnState;
@@ -56,12 +55,6 @@ class GlobalState {
} }
Future<ProviderContainer> init(int version) async { Future<ProviderContainer> init(int version) async {
FlutterError.onError = (details) {
commonPrint.log(
'exception: ${details.exception} stack: ${details.stack}',
logLevel: LogLevel.warning,
);
};
coreSHA256 = const String.fromEnvironment('CORE_SHA256'); coreSHA256 = const String.fromEnvironment('CORE_SHA256');
isPre = const String.fromEnvironment('APP_ENV') != 'stable'; isPre = const String.fromEnvironment('APP_ENV') != 'stable';
await _initDynamicColor(); await _initDynamicColor();
@@ -95,9 +88,7 @@ class GlobalState {
configMap, configMap,
sync: (data) async { sync: (data) async {
final newConfigMap = data.configMap; final newConfigMap = data.configMap;
final config = newConfigMap != null final config = Config.realFromJson(newConfigMap);
? Config.fromJson(newConfigMap)
: Config(themeProps: defaultThemeProps);
await Future.wait([ await Future.wait([
database.restore(data.profiles, data.scripts, data.rules, data.links), database.restore(data.profiles, data.scripts, data.rules, data.links),
preferences.saveConfig(config), preferences.saveConfig(config),

View File

@@ -228,7 +228,7 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dashboardState = ref.watch(dashboardStateProvider); final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.contentWidth / 280).ceil()), 8); final columns = max(4 * ((dashboardState.contentWidth / 280).ceil()), 8);
final spacing = 14.ap; final spacing = 14.mAp;
final children = [ final children = [
...dashboardState.dashboardWidgets ...dashboardState.dashboardWidgets
.where( .where(

View File

@@ -41,45 +41,54 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
final color = context.colorScheme.onSurfaceVariant.opacity80; final color = context.colorScheme.onSurfaceVariant.opacity80;
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: RepaintBoundary(
onPressed: () {}, child: CommonCard(
info: Info( onPressed: () {},
label: appLocalizations.networkSpeed, child: Consumer(
iconData: Icons.speed_sharp, builder: (_, ref, _) {
), final traffics = ref.watch(trafficsProvider).list;
child: Consumer( return Column(
builder: (_, ref, _) { children: [
final traffics = ref.watch(trafficsProvider).list; Padding(
return Stack( padding: baseInfoEdgeInsets.copyWith(bottom: 0),
children: [ child: Row(
Positioned.fill( mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Padding( children: [
padding: EdgeInsets.all( Flexible(
16, child: InfoHeader(
).copyWith(bottom: 0, left: 0, right: 0), padding: EdgeInsets.zero,
child: LineChart( info: Info(
gradient: true, label: appLocalizations.networkSpeed,
color: Theme.of(context).colorScheme.primary, iconData: Icons.speed_sharp,
points: _getPoints(traffics), ),
),
),
SizedBox(width: 8),
Text(
_getLastTraffic(traffics).speedText,
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
],
), ),
), ),
), Flexible(
Positioned( child: Padding(
top: 0, padding: EdgeInsets.all(
right: 0, 16,
child: Transform.translate( ).copyWith(bottom: 0, left: 0, right: 0),
offset: Offset(-16, -20), child: LineChart(
child: Text( gradient: true,
_getLastTraffic(traffics).speedText, color: Theme.of(context).colorScheme.primary,
style: context.textTheme.bodySmall?.copyWith( points: _getPoints(traffics),
color: color,
), ),
), ),
), ),
), ],
], );
); },
}, ),
), ),
), ),
); );

View File

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

View File

@@ -30,7 +30,7 @@ class _EditProfileViewState extends State<EditProfileView> {
late final TextEditingController _labelController; late final TextEditingController _labelController;
late final TextEditingController _urlController; late final TextEditingController _urlController;
late final TextEditingController _autoUpdateDurationController; late final TextEditingController _autoUpdateDurationController;
late final bool _autoUpdate; late bool _autoUpdate;
String? _rawText; String? _rawText;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _fileInfoNotifier = ValueNotifier<FileInfo?>(null); final _fileInfoNotifier = ValueNotifier<FileInfo?>(null);

View File

@@ -5,6 +5,7 @@ import 'package:fl_clash/controller.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/features/overwrite/rule.dart'; import 'package:fl_clash/features/overwrite/rule.dart';
import 'package:fl_clash/models/models.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/database.dart';
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -28,10 +29,33 @@ class _OverwriteViewState extends ConsumerState<OverwriteView> {
super.initState(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonScaffold( return CommonScaffold(
title: appLocalizations.override, title: appLocalizations.override,
actions: [
CommonMinFilledButtonTheme(
child: FilledButton(
onPressed: _handlePreview,
child: Text(appLocalizations.preview),
),
),
SizedBox(width: 8),
],
body: CustomScrollView( body: CustomScrollView(
slivers: [_Title(widget.profileId), _Content(widget.profileId)], slivers: [_Title(widget.profileId), _Content(widget.profileId)],
), ),
@@ -341,7 +365,9 @@ class _ScriptContent extends ConsumerWidget {
void _handleChange(WidgetRef ref, int scriptId) { void _handleChange(WidgetRef ref, int scriptId) {
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) { ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
return state.copyWith(scriptId: scriptId); return state.copyWith(
scriptId: state.scriptId == scriptId ? null : scriptId,
);
}); });
} }

View File

@@ -23,6 +23,7 @@ class ProfilesView extends StatefulWidget {
class _ProfilesViewState extends State<ProfilesView> { class _ProfilesViewState extends State<ProfilesView> {
Function? applyConfigDebounce; Function? applyConfigDebounce;
bool _isUpdating = false;
void _handleShowAddExtendPage() { void _handleShowAddExtendPage() {
showExtend( showExtend(
@@ -40,6 +41,10 @@ class _ProfilesViewState extends State<ProfilesView> {
} }
Future<void> _updateProfiles(List<Profile> profiles) async { Future<void> _updateProfiles(List<Profile> profiles) async {
if (_isUpdating == true) {
return;
}
_isUpdating = true;
final List<UpdatingMessage> messages = []; final List<UpdatingMessage> messages = [];
final updateProfiles = profiles.map<Future>((profile) async { final updateProfiles = profiles.map<Future>((profile) async {
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
@@ -55,6 +60,7 @@ class _ProfilesViewState extends State<ProfilesView> {
if (messages.isNotEmpty) { if (messages.isNotEmpty) {
globalState.showAllUpdatingMessagesDialog(messages); globalState.showAllUpdatingMessagesDialog(messages);
} }
_isUpdating = false;
} }
List<Widget> _buildActions(List<Profile> profiles) { List<Widget> _buildActions(List<Profile> profiles) {
@@ -86,10 +92,10 @@ class _ProfilesViewState extends State<ProfilesView> {
} }
Widget _buildFAB() { Widget _buildFAB() {
return FloatingActionButton( return CommonFloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage, onPressed: _handleShowAddExtendPage,
child: const Icon(Icons.add), icon: const Icon(Icons.add),
label: context.appLocalizations.addProfile,
); );
} }
@@ -99,7 +105,7 @@ class _ProfilesViewState extends State<ProfilesView> {
builder: (_, ref, _) { builder: (_, ref, _) {
final isLoading = ref.watch(loadingProvider(LoadingTag.profiles)); final isLoading = ref.watch(loadingProvider(LoadingTag.profiles));
final state = ref.watch(profilesStateProvider); final state = ref.watch(profilesStateProvider);
final spacing = 14.ap; final spacing = 14.mAp;
return CommonScaffold( return CommonScaffold(
isLoading: isLoading, isLoading: isLoading,
title: appLocalizations.profiles, title: appLocalizations.profiles,

View File

@@ -410,19 +410,15 @@ class _DelayTestButtonState extends State<DelayTestButton>
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller.view, animation: _controller.view,
builder: (_, child) { builder: (_, child) {
return SizedBox( return FadeTransition(
width: 56, opacity: _animation,
height: 56, child: ScaleTransition(scale: _animation, child: child),
child: FadeTransition(
opacity: _animation,
child: ScaleTransition(scale: _animation, child: child),
),
); );
}, },
child: FloatingActionButton( child: CommonFloatingActionButton(
heroTag: null,
onPressed: _healthcheck, onPressed: _healthcheck,
child: const Icon(Icons.network_ping), label: appLocalizations.delayTest,
icon: const Icon(Icons.network_ping),
), ),
); );
} }

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/widgets/inherited.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ScrollOverBuilder extends StatefulWidget { class ScrollOverBuilder extends StatefulWidget {
@@ -35,58 +36,18 @@ class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
} }
} }
// class ProxiesActionsBuilder extends StatelessWidget { class FloatingActionButtonExtendedBuilder extends StatelessWidget {
// final Widget? child; final Widget Function(bool isExtend) builder;
// final Widget Function(
// ProxiesActionsState state,
// Widget? child,
// ) builder;
//
// const ProxiesActionsBuilder({
// super.key,
// required this.child,
// required this.builder,
// });
//
// @override
// Widget build(BuildContext context) {
// return Selector<AppState, ProxiesActionsState>(
// selector: (_, appState) => ProxiesActionsState(
// isCurrent: appState.currentLabel == "proxies",
// hasProvider: appState.providers.isNotEmpty,
// ),
// builder: (_, state, child) => builder(state, child),
// child: child,
// );
// }
// }
// class ActiveBuilder extends StatelessWidget { const FloatingActionButtonExtendedBuilder({super.key, required this.builder});
// final String label;
// final StateAndChildWidgetBuilder<bool> builder; @override
// final Widget? child; Widget build(BuildContext context) {
// final isExtended =
// const ActiveBuilder({ CommonScaffoldFabExtendedProvider.of(context)?.isExtended ?? true;
// super.key, return builder(isExtended);
// required this.label, }
// required this.builder, }
// required this.child,
// });
//
// @override
// Widget build(BuildContext context) {
// return Selector<AppState, bool>(
// selector: (_, appState) => appState.currentLabel == label,
// builder: (_, state, child) {
// return builder(
// state,
// child,
// );
// },
// child: child,
// );
// }
// }
typedef StateWidgetBuilder<T> = Widget Function(T state); typedef StateWidgetBuilder<T> = Widget Function(T state);

56
lib/widgets/button.dart Normal file
View File

@@ -0,0 +1,56 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class CommonFloatingActionButton extends StatelessWidget {
final VoidCallback? onPressed;
final Icon icon;
final String label;
const CommonFloatingActionButton({
super.key,
this.onPressed,
required this.icon,
required this.label,
});
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
.copyWith(
extendedIconLabelSpacing: 0,
extendedPadding: EdgeInsets.all(16),
),
),
child: FloatingActionButtonExtendedBuilder(
builder: (isExtended) {
return FloatingActionButton.extended(
heroTag: null,
icon: icon,
onPressed: onPressed,
isExtended: true,
label: AnimatedSize(
alignment: Alignment.centerLeft,
duration: midDuration,
curve: Curves.easeOutBack,
child: AnimatedOpacity(
duration: midDuration,
opacity: isExtended ? 1.0 : 0.4,
curve: Curves.linear,
child: isExtended
? Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(label, softWrap: false),
)
: const SizedBox.shrink(),
),
),
);
},
),
);
}
}

View File

@@ -29,7 +29,7 @@ class InfoHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
EdgeInsetsGeometry nextPadding = (padding ?? baseInfoEdgeInsets); EdgeInsetsGeometry nextPadding = (padding ?? baseInfoEdgeInsets);
if (actions.isNotEmpty) { if (actions.isNotEmpty) {
nextPadding = nextPadding.subtract(EdgeInsets.symmetric(vertical: 8.ap)); nextPadding = nextPadding.subtract(EdgeInsets.symmetric(vertical: 8.mAp));
} }
return Padding( return Padding(
padding: nextPadding, padding: nextPadding,

View File

@@ -1,7 +1,8 @@
import 'dart:math' as math;
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'dart:math' as math;
typedef WrapBuilder = Widget Function(Widget child); typedef WrapBuilder = Widget Function(Widget child);
@@ -27,10 +28,10 @@ class Grid extends MultiChildRenderObjectWidget {
TextDirection? textDirection, TextDirection? textDirection,
this.mainAxisExtent, this.mainAxisExtent,
List<Widget>? children, List<Widget>? children,
}) : crossAxisCount = crossAxisCount ?? 1, }) : crossAxisCount = crossAxisCount ?? 1,
axisDirection = axisDirection ?? AxisDirection.down, axisDirection = axisDirection ?? AxisDirection.down,
textDirection = textDirection ?? TextDirection.ltr, textDirection = textDirection ?? TextDirection.ltr,
super(children: children ?? const []); super(children: children ?? const []);
const Grid.baseGap({ const Grid.baseGap({
Key? key, Key? key,
@@ -42,15 +43,15 @@ class Grid extends MultiChildRenderObjectWidget {
double? mainAxisExtent, double? mainAxisExtent,
List<Widget>? children, List<Widget>? children,
}) : this( }) : this(
key: key, key: key,
mainAxisSpacing: mainAxisSpacing, mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing, crossAxisSpacing: crossAxisSpacing,
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
axisDirection: axisDirection, axisDirection: axisDirection,
textDirection: textDirection, textDirection: textDirection,
mainAxisExtent: mainAxisExtent, mainAxisExtent: mainAxisExtent,
children: children, children: children,
); );
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
@@ -65,10 +66,7 @@ class Grid extends MultiChildRenderObjectWidget {
} }
@override @override
void updateRenderObject( void updateRenderObject(BuildContext context, RenderGrid renderObject) {
BuildContext context,
RenderGrid renderObject,
) {
renderObject renderObject
..mainAxisSpacing = mainAxisSpacing ..mainAxisSpacing = mainAxisSpacing
..mainAxisExtent = mainAxisExtent ..mainAxisExtent = mainAxisExtent
@@ -90,12 +88,12 @@ class RenderGrid extends RenderBox
required AxisDirection axisDirection, required AxisDirection axisDirection,
required TextDirection textDirection, required TextDirection textDirection,
double? mainAxisExtent, double? mainAxisExtent,
}) : _crossAxisCount = crossAxisCount, }) : _crossAxisCount = crossAxisCount,
_crossAxisSpacing = crossAxisSpacing, _crossAxisSpacing = crossAxisSpacing,
_mainAxisSpacing = mainAxisSpacing, _mainAxisSpacing = mainAxisSpacing,
_axisDirection = axisDirection, _axisDirection = axisDirection,
_textDirection = textDirection, _textDirection = textDirection,
_mainAxisExtent = mainAxisExtent; _mainAxisExtent = mainAxisExtent;
int _crossAxisCount; int _crossAxisCount;
@@ -214,15 +212,10 @@ class RenderGrid extends RenderBox
GridParentData childParentData, GridParentData childParentData,
int crossAxisCount, int crossAxisCount,
) { ) {
return math.min( return math.min(childParentData.crossAxisCellCount ?? 1, crossAxisCount);
childParentData.crossAxisCellCount ?? 1,
crossAxisCount,
);
} }
Size _computeSize({ Size _computeSize({required BoxConstraints constraints}) {
required BoxConstraints constraints,
}) {
final crossAxisExtent = mainAxis == Axis.vertical final crossAxisExtent = mainAxis == Axis.vertical
? constraints.maxWidth ? constraints.maxWidth
: constraints.maxHeight; : constraints.maxHeight;
@@ -245,11 +238,13 @@ class RenderGrid extends RenderBox
? BoxConstraints.tightFor(width: crossAxisExtent) ? BoxConstraints.tightFor(width: crossAxisExtent)
: BoxConstraints.tightFor(height: crossAxisExtent); : BoxConstraints.tightFor(height: crossAxisExtent);
_layoutChild(child, childConstraints, parentUsesSize: true); _layoutChild(child, childConstraints, parentUsesSize: true);
mainAxisExtent = mainAxisExtent = mainAxis == Axis.vertical
mainAxis == Axis.vertical ? child.size.height : child.size.width; ? child.size.height
: child.size.width;
} else { } else {
final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1; final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1;
mainAxisExtent = (this.mainAxisExtent ?? stride) * mainAxisCellCount - mainAxisExtent =
(this.mainAxisExtent ?? stride) * mainAxisCellCount -
mainAxisSpacing; mainAxisSpacing;
childParentData.realMainAxisExtent = mainAxisExtent; childParentData.realMainAxisExtent = mainAxisExtent;
final childSize = mainAxis == Axis.vertical final childSize = mainAxis == Axis.vertical
@@ -265,9 +260,7 @@ class RenderGrid extends RenderBox
? Offset(crossAxisOffset, mainAxisOffset) ? Offset(crossAxisOffset, mainAxisOffset)
: Offset(mainAxisOffset, crossAxisOffset); : Offset(mainAxisOffset, crossAxisOffset);
childParentData.offset = offset; childParentData.offset = offset;
final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing; final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing;
for (int i = 0; i < crossAxisCellCount; i++) { for (int i = 0; i < crossAxisCellCount; i++) {
offsets[origin.crossAxisIndex + i] = nextOffset; offsets[origin.crossAxisIndex + i] = nextOffset;
} }
@@ -281,7 +274,8 @@ class RenderGrid extends RenderBox
final childParentData = _getParentData(child); final childParentData = _getParentData(child);
final offset = childParentData.offset; final offset = childParentData.offset;
final crossAxisOffset = offset.getCrossAxisOffset(mainAxis); final crossAxisOffset = offset.getCrossAxisOffset(mainAxis);
final mainAxisOffset = mainAxisExtent - final mainAxisOffset =
mainAxisExtent -
offset.getMainAxisOffset(mainAxis) - offset.getMainAxisOffset(mainAxis) -
childParentData.realMainAxisExtent!; childParentData.realMainAxisExtent!;
final newOffset = mainAxis == Axis.vertical final newOffset = mainAxis == Axis.vertical
@@ -365,15 +359,11 @@ class GridItem extends ParentDataWidget<GridParentData> {
@override @override
Type get debugTypicalAncestorWidgetClass => GridItem; Type get debugTypicalAncestorWidgetClass => GridItem;
GridItem wrap({ GridItem wrap({required WrapBuilder builder}) {
required WrapBuilder builder,
}) {
return GridItem( return GridItem(
mainAxisCellCount: mainAxisCellCount, mainAxisCellCount: mainAxisCellCount,
crossAxisCellCount: crossAxisCellCount, crossAxisCellCount: crossAxisCellCount,
child: builder( child: builder(child),
child,
),
); );
} }
} }
@@ -395,11 +385,13 @@ _Origin _getOrigin(List<double> offsets, int crossAxisCount) {
} }
int start = 0; int start = 0;
int span = 0; int span = 0;
for (int j = 0; for (
span < crossAxisCount && int j = 0;
j < length && span < crossAxisCount &&
length - j >= crossAxisCount - span; j < length &&
j++) { length - j >= crossAxisCount - span;
j++
) {
if (offset.moreOrEqual(offsets[j])) { if (offset.moreOrEqual(offsets[j])) {
span++; span++;
if (span == crossAxisCount) { if (span == crossAxisCount) {

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class CommonScaffoldBackActionProvider extends InheritedWidget {
final VoidCallback? backAction;
const CommonScaffoldBackActionProvider({
super.key,
required this.backAction,
required super.child,
});
static CommonScaffoldBackActionProvider? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
}
@override
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) =>
backAction != oldWidget.backAction;
}
class CommonScaffoldFabExtendedProvider extends InheritedWidget {
final bool isExtended;
const CommonScaffoldFabExtendedProvider({
super.key,
required this.isExtended,
required super.child,
});
static CommonScaffoldFabExtendedProvider? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<
CommonScaffoldFabExtendedProvider
>();
}
@override
bool updateShouldNotify(CommonScaffoldFabExtendedProvider oldWidget) =>
isExtended != oldWidget.isExtended;
}

View File

@@ -3,8 +3,10 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/pop_scope.dart'; import 'package:fl_clash/widgets/pop_scope.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'chip.dart'; import 'chip.dart';
import 'inherited.dart';
typedef OnKeywordsUpdateCallback = void Function(List<String> keywords); typedef OnKeywordsUpdateCallback = void Function(List<String> keywords);
@@ -47,8 +49,8 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> { class CommonScaffoldState extends State<CommonScaffold> {
late final ValueNotifier<AppBarState> _appBarState; late final ValueNotifier<AppBarState> _appBarState;
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loadingNotifier = ValueNotifier(false); final ValueNotifier<bool> _loadingNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isFabExtendedNotifier = ValueNotifier(true);
final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]); final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]);
final _textController = TextEditingController(); final _textController = TextEditingController();
@@ -83,12 +85,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
_updateSearchState((state) => state?.copyWith(query: '')); _updateSearchState((state) => state?.copyWith(query: ''));
} }
set floatingActionButton(Widget? floatingActionButton) {
if (_floatingActionButton.value != floatingActionButton) {
_floatingActionButton.value = floatingActionButton;
}
}
Widget _buildSearchingAppBarTheme(Widget child) { Widget _buildSearchingAppBarTheme(Widget child) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme; final ColorScheme colorScheme = theme.colorScheme;
@@ -156,7 +152,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
void dispose() { void dispose() {
_appBarState.dispose(); _appBarState.dispose();
_textController.dispose(); _textController.dispose();
_floatingActionButton.dispose(); _isFabExtendedNotifier.dispose();
_loadingNotifier.dispose(); _loadingNotifier.dispose();
super.dispose(); super.dispose();
} }
@@ -350,17 +346,31 @@ class CommonScaffoldState extends State<CommonScaffold> {
); );
return Scaffold( return Scaffold(
appBar: _buildAppBar(backActionProvider?.backAction), appBar: _buildAppBar(backActionProvider?.backAction),
body: body, body: NotificationListener<UserScrollNotification>(
child: body,
onNotification: (notification) {
if (notification.direction == ScrollDirection.reverse) {
_isFabExtendedNotifier.value = false;
} else if (notification.direction == ScrollDirection.forward) {
_isFabExtendedNotifier.value = true;
}
return true;
},
),
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset, resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
floatingActionButton: floatingActionButton: widget.floatingActionButton != null
widget.floatingActionButton ?? ? ValueListenableBuilder<bool>(
ValueListenableBuilder<Widget?>( valueListenable: _isFabExtendedNotifier,
valueListenable: _floatingActionButton, builder: (_, isExtended, child) {
builder: (_, value, _) { return CommonScaffoldFabExtendedProvider(
return value ?? SizedBox(); isExtended: isExtended,
}, child: child!,
), );
},
child: widget.floatingActionButton,
)
: null,
); );
} }
} }
@@ -372,25 +382,6 @@ List<Widget> genActions(List<Widget> actions, {double? space}) {
]; ];
} }
class CommonScaffoldBackActionProvider extends InheritedWidget {
final VoidCallback? backAction;
const CommonScaffoldBackActionProvider({
super.key,
required this.backAction,
required super.child,
});
static CommonScaffoldBackActionProvider? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
}
@override
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) =>
backAction != oldWidget.backAction;
}
class BaseScaffold extends StatelessWidget { class BaseScaffold extends StatelessWidget {
final String title; final String title;
final List<Widget> actions; final List<Widget> actions;

View File

@@ -1,6 +1,7 @@
export 'activate_box.dart'; export 'activate_box.dart';
export 'animate_grid.dart'; export 'animate_grid.dart';
export 'builder.dart'; export 'builder.dart';
export 'button.dart';
export 'card.dart'; export 'card.dart';
export 'chip.dart'; export 'chip.dart';
export 'color_scheme_box.dart'; export 'color_scheme_box.dart';
@@ -13,6 +14,7 @@ export 'fade_box.dart';
export 'float_layout.dart'; export 'float_layout.dart';
export 'grid.dart'; export 'grid.dart';
export 'icon.dart'; export 'icon.dart';
export 'inherited.dart';
export 'input.dart'; export 'input.dart';
export 'keep_scope.dart'; export 'keep_scope.dart';
export 'line_chart.dart'; export 'line_chart.dart';

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.92+2026012301 version: 0.8.92+2026020201
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'

View File

@@ -369,6 +369,15 @@ class BuildCommand extends Command {
.map((e) => e.arch!) .map((e) => e.arch!)
.toList(); .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 { Future<void> _getLinuxDependencies(Arch arch) async {
await Build.exec(Build.getExecutable('sudo apt update -y')); await Build.exec(Build.getExecutable('sudo apt update -y'));
await Build.exec( await Build.exec(
@@ -412,7 +421,7 @@ class BuildCommand extends Command {
await Build.exec( await Build.exec(
name: name, name: name,
Build.getExecutable( 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, 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') { if (out != 'app') {
return; return;
} }
switch (target) { switch (target) {
case Target.windows: case Target.windows:
final token = target != Target.android
? await Build.calcSha256(corePaths.first)
: null;
Build.buildHelper(target, token!);
_buildDistributor( _buildDistributor(
target: target, target: target,
targets: 'exe,zip', targets: 'exe,zip',
args: args: ' --description $archName',
' --description $archName --build-dart-define=CORE_SHA256=$token',
env: env, env: env,
); );
return; return;