Files
pangolin/client/lib/screens/settings_page.dart
T
wangjia 8584f0459e feat(client): 自动连接开关 + Windows 开机自启改任务计划程序
- 新增「自动连接」设置开关(默认关,持久化):启动 app 进登录态主界面后,等节点就绪
  自动连一次(_RootFlowState 一次性 listenManual nodesProvider → toggle())。全平台生效。
- Windows 开机自启改用 schtasks /rl highest 登录触发任务:本应用 requireAdministrator,
  原注册表 Run 键(非提权)开机会弹 UAC/起不来 → 任务计划静默提权拉起。macOS/Linux 仍用
  launch_at_startup 登录项。
- 设置页功能组加「自动连接」行;新增 autoConnect/autoConnectSub 文案。
- _load 加 mounted 守卫(异步初始化期间已 dispose 不再写 state)。
- 测试:autoConnect 默认关 / _load 读持久化 / setAutoConnect 落盘。

注:移动端真·常连(Android Always-on + iOS/iPad On-Demand,无 UI 自连)是更大的原生活,
已单列 todo #14 后续做;本轮移动端只有「打开 app 自动连接」。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:46:06 +08:00

151 lines
5.7 KiB
Dart

// settings_page.dart — 设置(desktop 一级页)
//
// 对照 ui_kits/desktop/dapp.jsx DSettings(精简):兑换入口 + 语言段控 +
// 深色外观 + 协议 + 版本。开关组(开机自启/智能分流/Kill-switch)待 l10n 补齐后加。
import 'package:flutter/material.dart';
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 ConsumerWidget {
const SettingsPage({super.key});
@override
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);
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(32, 16, 32, 24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// 兑换入口
_Card(children: [
_Row(title: t.redeemEntry, sub: t.proMember, last: true,
right: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), onTap: () {}),
]),
const SizedBox(height: 16),
// 功能开关组
_Card(children: [
_Row(title: t.autostart, sub: t.autostartSub, right: sw(s.autostart, settings.setAutostart)),
_Row(title: t.autoConnect, sub: t.autoConnectSub, right: sw(s.autoConnect, settings.setAutoConnect)),
_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),
// 配置组
_Card(children: [
_Row(title: t.language, right: _LangSwitch(lang: lang, onPick: (l) => ref.read(localeProvider.notifier).state = l)),
_Row(
title: t.darkAppearance,
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('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(version, style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))),
]),
]),
),
);
}
}
class _Card extends StatelessWidget {
const _Card({required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
final c = context.pangolin;
return Container(
decoration: BoxDecoration(
color: c.surface,
border: Border.all(color: c.border),
borderRadius: BorderRadius.circular(PangolinRadius.lg),
boxShadow: PangolinShadow.sm,
),
clipBehavior: Clip.antiAlias,
child: Column(children: children),
);
}
}
class _Row extends StatelessWidget {
const _Row({required this.title, this.sub, required this.right, this.last = false, this.onTap});
final String title;
final String? sub;
final Widget right;
final bool last;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final c = context.pangolin;
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(
border: last ? null : Border(bottom: BorderSide(color: c.border)),
),
child: Row(children: [
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, style: PangolinText.body.copyWith(color: c.fg1, fontSize: 14, fontWeight: FontWeight.w500)),
if (sub != null) Text(sub!, style: PangolinText.caption.copyWith(color: c.fg3)),
]),
),
right,
]),
),
);
}
}
class _LangSwitch extends StatelessWidget {
const _LangSwitch({required this.lang, required this.onPick});
final AppLang lang;
final ValueChanged<AppLang> onPick;
@override
Widget build(BuildContext context) {
final c = context.pangolin;
Widget seg(AppLang v, String label) {
final on = lang == v;
return GestureDetector(
onTap: () => onPick(v),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: on ? c.accent : Colors.transparent,
borderRadius: BorderRadius.circular(PangolinRadius.full),
),
child: Text(label,
style: PangolinText.caption.copyWith(
color: on ? c.fgOnAccent : c.fg3, fontWeight: FontWeight.w700, fontSize: 12)),
),
);
}
return Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)),
child: Row(mainAxisSize: MainAxisSize.min, children: [seg(AppLang.zh, '中文'), seg(AppLang.en, 'EN')]),
);
}
}