// settings_provider.dart — 本地应用设置(持久化 + 副作用落地)。 // // 三个开关均本地持久化(shared_preferences),并落地真实行为: // - killSwitch → VpnBridge.setKillSwitch(改 TUN strict_route 并重载内核) // - autostart → 桌面端原生登录项(launch_at_startup) // - smartRoute → 持久化为偏好,连接时作为 split_cn 下发(geoip-cn 直连,见 #5) import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../bridge/log.dart'; import '../bridge/vpn_bridge_provider.dart'; class AppSettings { const AppSettings({ this.autostart = true, this.autoConnect = false, this.smartRoute = true, this.killSwitch = true, this.loaded = false, }); final bool autostart; final bool autoConnect; // 启动 app 时自动连接(opt-in,默认关) final bool smartRoute; final bool killSwitch; final bool loaded; // 是否已从持久化加载完(自动连接要等它 true 再读 autoConnect,避免竞态) AppSettings copyWith({bool? autostart, bool? autoConnect, bool? smartRoute, bool? killSwitch, bool? loaded}) => AppSettings( autostart: autostart ?? this.autostart, autoConnect: autoConnect ?? this.autoConnect, smartRoute: smartRoute ?? this.smartRoute, killSwitch: killSwitch ?? this.killSwitch, loaded: loaded ?? this.loaded, ); } const _kAutostart = 'set_autostart'; const _kAutoConnect = 'set_auto_connect'; const _kSmartRoute = 'set_smart_route'; const _kKillSwitch = 'set_kill_switch'; // Windows 任务计划名(开机自启用最高权限任务,绕开注册表 Run 起不来/弹 UAC)。 const _kWinTaskName = 'PangolinAutostart'; class SettingsController extends StateNotifier { SettingsController(this._ref) : super(const AppSettings()) { _load(); } final Ref _ref; final bool _desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux; Future _load() async { var s = const AppSettings(loaded: true); // 失败也标 loaded:true,自动连接才不会永久等 try { final p = await SharedPreferences.getInstance(); if (!mounted) return; // 异步初始化期间已 dispose:别再写 state。 s = AppSettings( autostart: p.getBool(_kAutostart) ?? true, autoConnect: p.getBool(_kAutoConnect) ?? false, smartRoute: p.getBool(_kSmartRoute) ?? true, killSwitch: p.getBool(_kKillSwitch) ?? true, loaded: true, ); } catch (_) { // 测试/无平台:用默认值(已标 loaded)。 } if (!mounted) return; state = s; logLine('Settings', 'loaded autoConnect=${s.autoConnect} autostart=${s.autostart}'); _applyKillSwitch(s.killSwitch); if (_desktop) await _applyAutostart(s.autostart); } Future setAutostart(bool v) async { state = state.copyWith(autostart: v); await _persist(_kAutostart, v); if (_desktop) await _applyAutostart(v); } Future setAutoConnect(bool v) async { state = state.copyWith(autoConnect: v); await _persist(_kAutoConnect, v); logLine('Settings', 'setAutoConnect=$v persisted'); } Future setSmartRoute(bool v) async { state = state.copyWith(smartRoute: v); await _persist(_kSmartRoute, v); } Future setKillSwitch(bool v) async { state = state.copyWith(killSwitch: v); await _persist(_kKillSwitch, v); _applyKillSwitch(v); } void _applyKillSwitch(bool v) { try { _ref.read(vpnBridgeProvider).setKillSwitch(on: v); } catch (_) { // 桥未就绪/测试环境:忽略,持久化值在下次连接时生效。 } } Future _applyAutostart(bool v) async { try { // Windows:本应用 requireAdministrator,注册表 Run 键(非提权)开机会弹 UAC/起不来, // 改用任务计划程序 /rl highest 登录触发任务 → 静默提权自启。 if (Platform.isWindows) { await _applyWindowsAutostart(v); return; } // macOS/Linux:登录项即可(无需管理员)。带 --autostart:登录拉起时隐藏到托盘 // (与 Windows 一致;main.dart 读该参数 windowManager.hide(),macOS 原生侧 orderOut 兜底)。 final info = await PackageInfo.fromPlatform(); launchAtStartup.setup( appName: info.appName, appPath: Platform.resolvedExecutable, args: const ['--autostart'], ); if (v) { await launchAtStartup.enable(); } else { await launchAtStartup.disable(); } } catch (_) { // 登录项注册失败(权限/调试运行)不应阻塞 UI。 } } // 用 schtasks 建/删最高权限登录触发任务。app 已是管理员 → 可建 /rl highest 任务, // 开机由计划程序静默提权拉起、不弹 UAC。/tr 含空格路径须内嵌引号。 // 带 --autostart 标志:让开机拉起的实例直接最小化到托盘(不弹主窗口),见 main.dart。 Future _applyWindowsAutostart(bool v) async { if (v) { final exe = Platform.resolvedExecutable; await Process.run('schtasks', [ '/create', '/tn', _kWinTaskName, '/tr', '"$exe" --autostart', '/sc', 'onlogon', '/rl', 'highest', '/f', ]); } else { await Process.run('schtasks', ['/delete', '/tn', _kWinTaskName, '/f']); } } Future _persist(String key, bool v) async { final p = await SharedPreferences.getInstance(); await p.setBool(key, v); } } final settingsProvider = StateNotifierProvider((ref) => SettingsController(ref)); /// 应用版本号(来自 pubspec,经 package_info_plus)。 final appVersionProvider = FutureProvider((ref) async { final info = await PackageInfo.fromPlatform(); return 'v${info.version}'; });