// settings_page.dart — 设置(desktop 一级页) // // 三组:连接(开机自启/自动连接/智能分流/kill-switch 开关)· 外观(语言/深色)· // 关于(协议/检查更新/版本 + Web 用户中心链接)。购买/订单入口已归 Account 页。 import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; import '../services/web_launch.dart'; import '../state/app_providers.dart'; import '../state/settings_provider.dart'; import '../state/update_provider.dart'; import '../widgets/pangolin_icons.dart'; import '../widgets/pangolin_toast.dart'; /// 「检查更新」手动触发:拉取 `$kApiBaseUrl/version`,失败/无更新走轻提示, /// 有更新则清掉「已忽略版本」→ 顶部更新 banner 显示 + toast(不弹窗)。 Future _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) async { final info = await ref.read(updateProvider.notifier).forceCheck(); if (!context.mounted) return; if (info == null) { showPangolinToast(context, t.updateCheckFailed); return; } if (!info.hasUpdate) { showPangolinToast(context, t.updateUpToDate); return; } // 有更新:清掉「已忽略版本」→ 顶部 banner 重新显示(不弹窗);toast 提示一下。 // 强制更新由 home_shell 的 listen 走不可关闭弹窗,这里无需处理。 ref.read(dismissedUpdateVersionProvider.notifier).state = null; showPangolinToast(context, t.updateAvailableTitle(info.latestVersion)); } 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 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: [ // ① 连接(功能开关组;开机自启是桌面端登录项概念,iOS/Android 无此能力,仅桌面显示) _SectionLabel(text: t.settingsGroupConn), _Card(children: [ if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) _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: 20), // ② 外观(语言 + 深色) _SectionLabel(text: t.settingsGroupAppearance), _Card(children: [ _Row(title: t.language, right: _LangSwitch(lang: lang, onPick: (l) => ref.read(localeProvider.notifier).set(l))), _Row( title: t.darkAppearance, sub: isDark ? t.stateOn : t.followLight, last: true, right: sw(isDark, (v) => ref.read(themeModeProvider.notifier).state = v ? ThemeMode.dark : ThemeMode.light), ), ]), const SizedBox(height: 20), // ③ 关于(协议 + 检查更新 + 版本 + 底部 Web 用户中心链接) _SectionLabel(text: t.settingsGroupAbout), _Card(children: [ _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: () => _checkForUpdate(context, ref, t)), _Row(title: 'Version', right: Text(version, style: PangolinText.mono.copyWith(fontSize: 13, color: c.fg3))), _Row(title: t.webUserCenter, last: true, right: Icon(PangolinIcons.externalLink, size: 18, color: c.fg3), onTap: () => openWebUserCenter(ref)), ]), ]), ), ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.text}); final String text; @override Widget build(BuildContext context) { final c = context.pangolin; return Padding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 10), child: Text(text, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13)), ); } } class _Card extends StatelessWidget { const _Card({required this.children}); final List 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 onPick; @override Widget build(BuildContext context) { final c = context.pangolin; // 6 种语言用下拉(段控横排会挤)。当前语言以本地名 + 下拉箭头展示。 return PopupMenuButton( initialValue: lang, onSelected: onPick, color: c.surface, elevation: 8, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.md)), itemBuilder: (_) => [ for (final l in AppLang.values) PopupMenuItem( value: l, height: 42, child: Text(l.nativeLabel, style: PangolinText.caption.copyWith( color: l == lang ? c.accent : c.fg1, fontWeight: l == lang ? FontWeight.w700 : FontWeight.w500, fontSize: 13)), ), ], child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), child: Row(mainAxisSize: MainAxisSize.min, children: [ Text(lang.nativeLabel, style: PangolinText.caption .copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 12)), const SizedBox(width: 4), Icon(PangolinIcons.chevronDown, size: 14, color: c.fg3), ]), ), ); } }