Files
pangolin/client/lib/state/settings_provider.dart
T
wangjia 15133b8a72 feat(client/macos): 开机自启隐藏到托盘(--autostart),与 Windows 对齐
macOS 登录项(launch_at_startup)注册时带 args:['--autostart'];main() 读该参数
windowManager.hide();MainFlutterWindow.awakeFromNib 检测到 --autostart 时 orderOut
兜底(避免 storyboard Visible At Launch 闪窗)。常规启动无参数,正常显示窗口。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:27:30 +08:00

168 lines
5.9 KiB
Dart

// 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<AppSettings> {
SettingsController(this._ref) : super(const AppSettings()) {
_load();
}
final Ref _ref;
final bool _desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
Future<void> _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<void> setAutostart(bool v) async {
state = state.copyWith(autostart: v);
await _persist(_kAutostart, v);
if (_desktop) await _applyAutostart(v);
}
Future<void> setAutoConnect(bool v) async {
state = state.copyWith(autoConnect: v);
await _persist(_kAutoConnect, v);
logLine('Settings', 'setAutoConnect=$v persisted');
}
Future<void> setSmartRoute(bool v) async {
state = state.copyWith(smartRoute: v);
await _persist(_kSmartRoute, v);
}
Future<void> 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<void> _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<void> _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<void> _persist(String key, bool v) async {
final p = await SharedPreferences.getInstance();
await p.setBool(key, v);
}
}
final settingsProvider =
StateNotifierProvider<SettingsController, AppSettings>((ref) => SettingsController(ref));
/// 应用版本号(来自 pubspec,经 package_info_plus)。
final appVersionProvider = FutureProvider<String>((ref) async {
final info = await PackageInfo.fromPlatform();
return 'v${info.version}';
});