Files
FlClash/lib/common/migration.dart
T
chen08209 aaf934c2d1 refactor(app): rebuild core client, providers, models, database, managers, and window handling
Rework the application layer end to end: core IPC client, providers,
models, database, managers, and shared utilities, with riverpod 3.4.2
and freezed 4.0.1. Keep a rejected profile selected, push the empty
config to Core when a profile fails to build, and fall back to the
default activity icon for package icons.

Add opt-in proxy authentication that injects credentials into the
generated profile and the runtime config, force-clears profile-provided
skip-auth-prefixes so loopback stays covered, sends the credentials with
the app's own proxied requests, and withholds the Android VPN system
proxy while credentials are set. HelperCoreLease treats an unreachable
Helper as confirmation once the OS reports the Core pid gone, and the
lifecycle reconcile retries a parked stop, so the Core comes back after
the Helper service dies. Launch arguments seed LinkManager so a clash://
link raises a silently started window, the access control app list is
fetched on every page entry, the desktop header matches a standard title
bar height, and the tray icons ship as resolution-aware assets scoped per
platform. The version moves to 0.8.97+2026090101.

Rule.parse mirrors mihomo's ParseRulePayload: the type is
case-insensitive, MATCH rules keep no content field, a two-field rule is
payload without a target except for comma-payload types whose lone field
is the target, params are matched whole, and the wildcard and
REMATCH-NAME types the core accepts get their own actions instead of
collapsing into DOMAIN. Window routes show, hide and toggle through one
serialized queue and defers the macOS accessory activation switch until
a second after the last regular switch, so hotkey bursts no longer leave
stray Dock icons. The hotkey manager registers through rust_api instead
of hotkey_manager, the window header restyles the caption buttons after
the native Windows title bar, and the proxies tab keeps its tab
controller alive while the empty state animates in. Bootstrap reads the
Android dynamic palette from the plugin channel without the deprecated
CorePalette, dynamic_color moves to its material_ui release,
navigator_resizable 3.1.0 fixes an assertion when popping
mid-transition, reorderable_grid goes, and the SDK floor rises to 3.10.

Changelog: Rework the app layer and window handling, and add proxy authentication
2026-09-10 12:43:00 +08:00

161 lines
4.7 KiB
Dart

import 'package:fl_clash/database/database.dart';
import 'package:fl_clash/models/models.dart';
import 'preferences.dart';
import 'task.dart';
typedef MigrationTransform =
Future<MigrationData> Function(Map<String, Object?> configMap);
abstract interface class MigrationStore {
/// False when the backing store could not be opened at all, as opposed to a
/// store that opened but holds nothing.
Future<bool> get isAvailable;
Future<Map<String, Object?>?> getConfigMap();
Future<int> getVersion();
Future<Map<String, Object?>?> getClashConfigMap();
Future<void> restore(MigrationData data);
Future<bool> saveConfig(Config config);
Future<void> clearClashConfig();
Future<void> setVersion(int version);
}
class _AppMigrationStore implements MigrationStore {
const _AppMigrationStore();
@override
Future<bool> get isAvailable => preferences.isInit;
@override
Future<Map<String, Object?>?> getConfigMap() => preferences.getConfigMap();
@override
Future<int> getVersion() => preferences.getVersion();
@override
Future<Map<String, Object?>?> getClashConfigMap() =>
preferences.getClashConfigMap();
@override
Future<void> restore(MigrationData data) {
return database.restore(
data.profiles,
data.scripts,
data.rules,
data.links,
data.proxyGroups,
);
}
@override
Future<bool> saveConfig(Config config) => preferences.saveConfig(config);
@override
Future<void> clearClashConfig() => preferences.clearClashConfig();
@override
Future<void> setVersion(int version) => preferences.setVersion(version);
}
class Migration {
final MigrationStore _store;
final MigrationTransform _migrateV0;
Migration({required MigrationStore store, MigrationTransform? migrateV0})
: _store = store,
_migrateV0 = migrateV0 ?? oldToNowTask;
static const currentVersion = 1;
Future<Config> run() async {
final configMap = await _store.getConfigMap();
var oldVersion = await _store.getVersion();
Config? config;
if (oldVersion > currentVersion) {
throw StateError(
'Local data version $oldVersion is newer than $currentVersion.',
);
}
if (oldVersion == currentVersion) {
try {
config = Config.realFromJson(configMap);
} catch (_) {
if (!_isV0(configMap)) {
throw StateError(
'Local data is damaged. A reset is required to fix this issue.',
);
}
oldVersion = 0;
}
if (config != null) {
final storedDavPassword = _getStoredDavPassword(configMap);
final hasPlainTextDavPassword =
storedDavPassword != null &&
storedDavPassword == config.davProps?.password;
if (hasPlainTextDavPassword && !await _store.saveConfig(config)) {
throw StateError('Failed to obfuscate the legacy WebDAV password');
}
return config;
}
}
MigrationData data = MigrationData(configMap: configMap);
var shouldClearClashConfig = false;
if (oldVersion == 0) {
final clashConfigMap = await _store.getClashConfigMap();
if (_isV0(configMap) && configMap != null) {
final legacyConfigMap = Map<String, Object?>.from(configMap);
if (clashConfigMap != null) {
legacyConfigMap['patchClashConfig'] = clashConfigMap;
shouldClearClashConfig = true;
}
data = await _migrateV0(legacyConfigMap);
} else if (clashConfigMap != null) {
final currentConfigMap = Map<String, Object?>.from(
configMap ?? const {},
);
currentConfigMap.putIfAbsent('patchClashConfig', () => clashConfigMap);
data = MigrationData(configMap: currentConfigMap);
shouldClearClashConfig = true;
}
}
config = Config.realFromJson(data.configMap);
await _store.restore(data);
if (!await _store.saveConfig(config)) {
// An unopenable store is reported later by the corrupt-cache dialog,
// which offers a reset; failing here would hide that path.
if (await _store.isAvailable) {
throw StateError('Failed to save migrated preferences');
}
return config;
}
if (shouldClearClashConfig) {
await _store.clearClashConfig();
}
await _store.setVersion(currentVersion);
return config;
}
}
bool _isV0(Map<String, Object?>? configMap) =>
configMap?['proxiesStyle'] != null;
String? _getStoredDavPassword(Map<String, Object?>? configMap) {
final dav = configMap?['davProps'] ?? configMap?['dav'];
if (dav is! Map) {
return null;
}
final password = dav['password'];
return password is String && password.isNotEmpty ? password : null;
}
final migration = Migration(store: const _AppMigrationStore());