feat(client): P6 设置项落地真实行为(#6 6F,本地部分)
- settings_provider:三开关 shared_preferences 持久化 + 副作用落地: killSwitch→VpnBridge.setKillSwitch(on:);autostart→桌面原生登录项 (launch_at_startup);smartRoute 持久化为偏好(连接时作 split_cn,见 #5)。 - settings_page → ConsumerWidget,开关接 settingsProvider;协议标签 WireGuard→REALITY/Hysteria2;版本号取 pubspec(package_info_plus)。 - 新依赖:shared_preferences / package_info_plus / launch_at_startup。 flutter analyze 0 error;114 tests passed;settings golden 重生成。 待办:#5 智能分流的 server 端 geoip-cn 下发 + autostart 真机验证(需 release .app)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
// 本地演示态(暂无后端绑定),默认开,对照 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<bool> on) =>
|
||||
Switch(value: v, activeThumbColor: c.accent, onChanged: on);
|
||||
@@ -47,9 +41,9 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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))),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -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<AppSettings> {
|
||||
SettingsController(this._ref) : super(const AppSettings()) {
|
||||
_load();
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
final bool _desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
|
||||
|
||||
Future<void> _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<void> setAutostart(bool v) async {
|
||||
state = state.copyWith(autostart: v);
|
||||
await _persist(_kAutostart, v);
|
||||
if (_desktop) await _applyAutostart(v);
|
||||
}
|
||||
|
||||
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 {
|
||||
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<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}';
|
||||
});
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Reference in New Issue
Block a user