diff --git a/client/lib/screens/settings_page.dart b/client/lib/screens/settings_page.dart index 2320115..858ad4e 100644 --- a/client/lib/screens/settings_page.dart +++ b/client/lib/screens/settings_page.dart @@ -8,28 +8,22 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; import '../state/app_providers.dart'; +import '../state/settings_provider.dart'; import '../widgets/pangolin_icons.dart'; -class SettingsPage extends ConsumerStatefulWidget { +class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @override - ConsumerState createState() => _SettingsPageState(); -} - -class _SettingsPageState extends ConsumerState { - // 本地演示态(暂无后端绑定),默认开,对照 dapp.jsx DSettings。 - bool _autostart = true; - bool _smartRoute = true; - bool _killSwitch = true; - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final c = context.pangolin; final t = ref.watch(appTextProvider); final lang = ref.watch(localeProvider); final mode = ref.watch(themeModeProvider); final isDark = mode == ThemeMode.dark; + final s = ref.watch(settingsProvider); + final settings = ref.read(settingsProvider.notifier); + final version = ref.watch(appVersionProvider).valueOrNull ?? '…'; Widget sw(bool v, ValueChanged on) => Switch(value: v, activeThumbColor: c.accent, onChanged: on); @@ -47,9 +41,9 @@ class _SettingsPageState extends ConsumerState { const SizedBox(height: 16), // 功能开关组 _Card(children: [ - _Row(title: t.autostart, sub: t.autostartSub, right: sw(_autostart, (v) => setState(() => _autostart = v))), - _Row(title: t.smartRoute, sub: t.smartRouteSub, right: sw(_smartRoute, (v) => setState(() => _smartRoute = v))), - _Row(title: t.killSwitch, sub: t.killSwitchSub, last: true, right: sw(_killSwitch, (v) => setState(() => _killSwitch = v))), + _Row(title: t.autostart, sub: t.autostartSub, right: sw(s.autostart, settings.setAutostart)), + _Row(title: t.smartRoute, sub: t.smartRouteSub, right: sw(s.smartRoute, settings.setSmartRoute)), + _Row(title: t.killSwitch, sub: t.killSwitchSub, last: true, right: sw(s.killSwitch, settings.setKillSwitch)), ]), const SizedBox(height: 16), // 配置组 @@ -60,9 +54,9 @@ class _SettingsPageState extends ConsumerState { sub: isDark ? t.stateOn : t.followLight, right: sw(isDark, (v) => ref.read(themeModeProvider.notifier).state = v ? ThemeMode.dark : ThemeMode.light), ), - _Row(title: t.protocol, right: Text('WireGuard', style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), + _Row(title: t.protocol, right: Text('REALITY / Hysteria2', style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), _Row(title: t.checkUpdate, right: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), onTap: () {}), - _Row(title: 'Version', last: true, right: Text('v1.0.0', style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), + _Row(title: 'Version', last: true, right: Text(version, style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), ]), ]), ), diff --git a/client/lib/state/settings_provider.dart b/client/lib/state/settings_provider.dart new file mode 100644 index 0000000..01ede93 --- /dev/null +++ b/client/lib/state/settings_provider.dart @@ -0,0 +1,108 @@ +// 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/vpn_bridge_provider.dart'; + +class AppSettings { + const AppSettings({this.autostart = true, this.smartRoute = true, this.killSwitch = true}); + final bool autostart; + final bool smartRoute; + final bool killSwitch; + + AppSettings copyWith({bool? autostart, bool? smartRoute, bool? killSwitch}) => AppSettings( + autostart: autostart ?? this.autostart, + smartRoute: smartRoute ?? this.smartRoute, + killSwitch: killSwitch ?? this.killSwitch, + ); +} + +const _kAutostart = 'set_autostart'; +const _kSmartRoute = 'set_smart_route'; +const _kKillSwitch = 'set_kill_switch'; + +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 { + try { + final p = await SharedPreferences.getInstance(); + state = AppSettings( + autostart: p.getBool(_kAutostart) ?? true, + smartRoute: p.getBool(_kSmartRoute) ?? true, + killSwitch: p.getBool(_kKillSwitch) ?? true, + ); + } catch (_) { + // 测试/无平台:保留默认值。 + } + _applyKillSwitch(state.killSwitch); + if (_desktop) await _applyAutostart(state.autostart); + } + + Future setAutostart(bool v) async { + state = state.copyWith(autostart: v); + await _persist(_kAutostart, v); + if (_desktop) await _applyAutostart(v); + } + + 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 { + final info = await PackageInfo.fromPlatform(); + launchAtStartup.setup(appName: info.appName, appPath: Platform.resolvedExecutable); + if (v) { + await launchAtStartup.enable(); + } else { + await launchAtStartup.disable(); + } + } catch (_) { + // 登录项注册失败(权限/调试运行)不应阻塞 UI。 + } + } + + 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}'; +}); diff --git a/client/macos/Flutter/GeneratedPluginRegistrant.swift b/client/macos/Flutter/GeneratedPluginRegistrant.swift index e84ed5a..16bb469 100644 --- a/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,11 @@ import FlutterMacOS import Foundation import flutter_secure_storage_macos +import package_info_plus +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 52b4e20..bf68abb 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -17,6 +17,9 @@ dependencies: http: ^1.2.1 # 控制面 HTTP 客户端(connect API) path_provider: ^2.1.3 # 获取应用支持目录(sing-box 配置路径) flutter_secure_storage: ^9.2.2 # JWT token 安全存储 + shared_preferences: ^2.5.5 + package_info_plus: ^9.0.1 + launch_at_startup: ^0.5.1 dev_dependencies: flutter_test: diff --git a/client/test/golden/goldens/desktop_settings.png b/client/test/golden/goldens/desktop_settings.png index 9caba9b..61dbc32 100644 Binary files a/client/test/golden/goldens/desktop_settings.png and b/client/test/golden/goldens/desktop_settings.png differ