diff --git a/client/lib/main.dart b/client/lib/main.dart index d43393f..86cd093 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -22,9 +22,7 @@ import 'system_tray.dart'; import 'widgets/auth_screen.dart'; import 'widgets/onboarding_screen.dart'; import 'widgets/pangolin_logo.dart'; - -/// 全局 ScaffoldMessenger key:供无 BuildContext 处(如自动连接)弹 SnackBar/toast。 -final rootMessengerKey = GlobalKey(); +import 'widgets/pangolin_toast.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -101,7 +99,6 @@ class _PangolinAppState extends ConsumerState { final mode = ref.watch(themeModeProvider); return MaterialApp( title: '穿山甲', - scaffoldMessengerKey: rootMessengerKey, debugShowCheckedModeBanner: false, theme: PangolinTheme.light, darkTheme: PangolinTheme.dark, @@ -155,13 +152,9 @@ class _RootFlowState extends ConsumerState { if (settings.autoConnect && phase == VpnPhase.off) { logLine('AutoConnect', 'triggering connect → ${node.code}'); ref.read(connectionProvider.notifier).toggle(); - // 自动连接 toast:告知用户「正在自动连接到 X」。 + // 自动连接 toast:「正在自动连接到 X」(主显示区内居中,见 pangolin_toast)。 final t = ref.read(appTextProvider); - rootMessengerKey.currentState?.showSnackBar(SnackBar( - content: Text(t.autoConnectingTo(node.localizedName(t.lang))), - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 3), - )); + if (mounted) showPangolinToast(context, t.autoConnectingTo(node.localizedName(t.lang))); } else { logLine('AutoConnect', 'NOT connecting (autoConnect=${settings.autoConnect} phase=$phase)'); } diff --git a/client/lib/widgets/pangolin_toast.dart b/client/lib/widgets/pangolin_toast.dart new file mode 100644 index 0000000..92aa061 --- /dev/null +++ b/client/lib/widgets/pangolin_toast.dart @@ -0,0 +1,76 @@ +// pangolin_toast.dart — 统一轻提示(toast)。真相源规格见 design/CONTRACT.md §2「Toast/SnackBar」。 +// +// 用 Overlay 实现(非 Material SnackBar),以完全控制布局: +// · 只在主显示区内(避开左侧栏:desktop 204 / tablet 232 / mobile 0)。 +// · 宽度自适应内容,上限为主显示区的 80%;超长则文本多行、居中。 +// · 跟随明暗主题:surface 底 + fg1 字 + border 描边 + md 圆角 + 柔和暖阴影,淡入轻起。 +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../core/responsive/form_factor.dart'; +import '../pangolin_theme.dart'; + +/// 在主显示区底部居中弹一条 toast(默认 3 秒)。需要一个能拿到 Overlay/主题的 context。 +void showPangolinToast(BuildContext context, String message, + {Duration duration = const Duration(seconds: 3)}) { + final overlay = Overlay.maybeOf(context, rootOverlay: true); + if (overlay == null) return; + late final OverlayEntry entry; + entry = OverlayEntry(builder: (_) => _PangolinToast(message: message)); + overlay.insert(entry); + Timer(duration, () { + if (entry.mounted) entry.remove(); + }); +} + +class _PangolinToast extends StatelessWidget { + const _PangolinToast({required this.message}); + final String message; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + // 侧栏宽度(避开它):取自各 shell 的 NavSidebar 宽度。 + final sidebar = switch (context.formFactor) { + FormFactor.desktop => 204.0, + FormFactor.tablet => 232.0, + FormFactor.mobile => 0.0, + }; + final w = MediaQuery.sizeOf(context).width; + final contentW = (w - sidebar).clamp(0.0, w); + return Positioned( + left: sidebar, + right: 0, + bottom: 28, + child: IgnorePointer( + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 240), + curve: Curves.easeOut, + builder: (_, t, child) => + Opacity(opacity: t, child: Transform.translate(offset: Offset(0, (1 - t) * 8), child: child)), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: contentW * 0.8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: c.surface, + borderRadius: BorderRadius.circular(PangolinRadius.md), + border: Border.all(color: c.border), + boxShadow: PangolinShadow.sm, + ), + child: Text( + message, + textAlign: TextAlign.center, + style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 64986a3..5437232 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,7 +1,7 @@ name: pangolin_vpn description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 publish_to: "none" -version: 1.0.25+26 +version: 1.0.26+27 environment: sdk: ^3.5.0 diff --git a/client/windows/installer/pangolin.iss b/client/windows/installer/pangolin.iss index 18f93ca..3fcd251 100644 --- a/client/windows/installer/pangolin.iss +++ b/client/windows/installer/pangolin.iss @@ -3,7 +3,7 @@ ; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在 ; ..\..\build\windows\x64\runner\Release #define MyAppName "穿山甲 Pangolin" -#define MyAppVersion "1.0.25" +#define MyAppVersion "1.0.26" #define MyAppPublisher "Pangolin" #define MyAppExeName "pangolin_vpn.exe" #define BuildDir "..\..\build\windows\x64\runner\Release" diff --git a/design/CONTRACT.md b/design/CONTRACT.md index d1aa79c..7e61452 100644 --- a/design/CONTRACT.md +++ b/design/CONTRACT.md @@ -32,13 +32,16 @@ | 节点行 | `widgets/server_tile.dart` / `nodes_page` `_NodeGridTile` | 双列网格 | | 指标卡 | `stats_page` `_MetricCard` | 复用 | | 段控/开关/行 | `account_page` 内组件 | 复用 | -| Toast / 通知条 | `pangolin_theme.dart` `snackBarTheme`(全局) | 新增组件(无原型) | +| Toast / 通知条 | `widgets/pangolin_toast.dart` `showPangolinToast` | 新增组件(无原型) | -### Toast / SnackBar(通知条)规格 -跨平台轻提示(如自动连接「正在自动连接到 X」)。**无 React 原型 → 此处即真相源**;全部走语义 token,明暗自动适配: -- 底色 `--surface`(浅=白 / 深=`#221E19`);文字 `--fg1`,Manrope 14/600;描边 `--border` 1px。 -- 圆角 `md`(10);`SnackBarBehavior.floating`;阴影柔和暖调(elevation≈6,`rgba(45,30,20,…)` 体系)。 -- 绝不用 Material 默认黑底。Flutter canonical:`PangolinTheme._build` 的 `snackBarTheme`(改样式只改此一处,全 app 一致)。 +### Toast / 通知条规格 +跨平台轻提示(如自动连接「正在自动连接到 X」)。**无 React 原型 → 此处即真相源**;全部走语义 token,明暗自动适配。 +**外观**:底 `--surface`(浅=白 / 深=`#221E19`)+ 文字 `--fg1`(Manrope 14/600,居中)+ 描边 `--border` 1px + 圆角 `md`(10)+ 柔和暖阴影(`shadow-sm`);淡入并轻微上移(240ms ease-out)。绝不用 Material 默认黑底。 +**布局**(铁律): +- **只在主显示区内**,不覆盖左侧栏(left = 侧栏宽:desktop 204 / tablet 232 / mobile 0)。 +- **宽度自适应内容**,上限 = 主显示区宽度的 **80%**;超过则**文本多行**。 +- 在主显示区内**水平居中**,贴底(bottom 28)。 +- Canonical 实现:`widgets/pangolin_toast.dart` `showPangolinToast(context, msg)`(Overlay 实现,非 SnackBar,以完全控制上述布局)。`pangolin_theme` 的 `snackBarTheme` 只作普通 SnackBar 的兜底底色样式。 ## 3. 页面像素规格(取自 tabapp.jsx)