diff --git a/client/lib/main.dart b/client/lib/main.dart index 4fc3bbb..005514f 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -20,7 +20,7 @@ import 'host/boot_report.dart'; import 'host/device_id.dart'; import 'host/link.dart'; import 'host/metric_format.dart'; -import 'host/pmon_core.dart'; +import 'host/inkwatch_core.dart'; import 'host/resident_service.dart'; import 'host/startup_log.dart'; import 'platform/android_foreground.dart'; @@ -61,14 +61,14 @@ Future main() async { 'platform': Platform.operatingSystem, 'dir': ConfigStore.dir, }); - // 掉帧诊断(默认关;`--dart-define=PMON_LOG_JANK=true` 打开,见 platform/frame_jank_log.dart) + // 掉帧诊断(默认关;`--dart-define=INKWATCH_LOG_JANK=true` 打开,见 platform/frame_jank_log.dart) installJankLogger(); // 主题偏好是显式的应用内设置(不跟随系统):runApp 前读一次, // 避免首屏先闪一下浅色再跳到用户上次选的深色。ConfigStore.load() // 自带兜底、不抛异常,故不存在"卡在启动"的路径。 final initialCfg = await ConfigStore.load(); appThemeMode.value = themeModeFromConfig(initialCfg.themeMode); - runApp(PmonApp()); + runApp(InkwatchApp()); } /// 应用内**显式**主题偏好 —— 不跟随系统亮度。 @@ -98,8 +98,8 @@ Future setAppThemeMode(ThemeMode mode) async { } } -class PmonApp extends StatelessWidget { - const PmonApp({super.key}); +class InkwatchApp extends StatelessWidget { + const InkwatchApp({super.key}); @override Widget build(BuildContext context) { @@ -111,8 +111,8 @@ class PmonApp extends StatelessWidget { // 主题必须**只有一处装配**(ui/theme.dart): // 否则系统控件(输入框/菜单/对话框/SnackBar)与我们自绘的卡片会各是一套色与圆角, // 表现为"弹窗像另一个应用"。旧版这里是就地 new 了一个只有 seedColor 的 ThemeData。 - theme: buildPmonTheme(), - darkTheme: buildPmonDarkTheme(), + theme: buildInkwatchTheme(), + darkTheme: buildInkwatchDarkTheme(), // 由应用内显式偏好驱动,不采用系统跟随模式。 themeMode: mode, home: HomeShell(), @@ -139,7 +139,7 @@ class HomeShell extends StatefulWidget { } class _HomeShellState extends State { - PmonPalette get _p => PmonPalette.of(context); + InkwatchPalette get _p => InkwatchPalette.of(context); int _tab = 0; bool _viewerEverOpened = false; @@ -182,7 +182,7 @@ class _HomeShellState extends State { // body 里发生什么它一无所知。不订阅的话,副屏底下会一直挂着 // 「采集 / 查看」两个页签(真机反馈"底下的导航键没有隐藏")。 bottomNavigationBar: ValueListenableBuilder( - valueListenable: PmonEinkBoardMode.active, + valueListenable: InkwatchEinkBoardMode.active, builder: (context, boardMode, _) => boardMode ? SizedBox.shrink() : DecoratedBox( @@ -223,14 +223,14 @@ class ControlPanelPage extends StatefulWidget { class _ControlPanelPageState extends State with WidgetsBindingObserver { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); late final TextEditingController _urlCtrl; late final TextEditingController _deviceIdCtrl; ClientAgent? _agent; - /// 常驻采集的状态/动作(P0-5)。只做"调 pmon-install CLI + 显示结果", + /// 常驻采集的状态/动作(P0-5)。只做"调 inkwatch-install CLI + 显示结果", /// 判据在 CLI 那一侧(单一真相源),本类不自己查进程/查文件。 final ResidentService _resident = ResidentService(); @@ -506,7 +506,7 @@ class _ControlPanelPageState extends State _hardwareId = hwId; }); - // 启动诊断口(默认关,`--dart-define=PMON_LOG_BOOT=true` 才输出)。 + // 启动诊断口(默认关,`--dart-define=INKWATCH_LOG_BOOT=true` 才输出)。 // // 用途:**覆盖安装残留**类故障的唯一取证口 —— release 包不可 `adb run-as`, // 读不到私有目录里的 `config.json` / `viewer_cache.json`,只能让应用自己报。 @@ -557,9 +557,9 @@ class _ControlPanelPageState extends State /// 文案由 Rust 核心生成(`runtime_guidance`),避免在 Dart 侧再写一份。 Future _loadAndroidGuidance() async { try { - final plan = PmonCore.instance.planAutostart( + final plan = InkwatchCore.instance.planAutostart( exePath: '', // Android 的自启是构建期清单声明,不需要可执行文件路径 - label: 'com.pmon.pmon_client', + label: 'com.inkwatch.inkwatch_client', ); final guidance = (plan['runtime_guidance'] as List?)?.cast() ?? const []; @@ -637,7 +637,7 @@ class _ControlPanelPageState extends State /// /// ── 为什么 agent 路径取自 CLI 而不是本地推导 ─────────────────── /// 沙箱 GUI 里 `installBinDirFor(macOS, homeDir)` 算出的 `homeDir` 就是容器 - /// 路径,拼出来的是"容器/…/bin/pmon-agent",用它 pgrep 永远匹配不到(更糟: + /// 路径,拼出来的是"容器/…/bin/inkwatch-agent",用它 pgrep 永远匹配不到(更糟: /// 会得到一个看起来确定的 `notRunning`)。CLI 是非沙箱进程,`status --json` /// 报的 `installed.agent_path` 才是 launchd 实际启动的那条。 /// @@ -650,7 +650,7 @@ class _ControlPanelPageState extends State final lockHeld = await AgentLock.isHeldByOther(); var presence = AgentPresence.notApplicable; - if (currentPlatform == PmonPlatform.macOS && !lockHeld) { + if (currentPlatform == InkwatchPlatform.macOS && !lockHeld) { // 锁已经命中就不必再花一次 CLI 往返;两层只要有一层说"有人在跑"就够 final st = await _resident.status(); if (st.agentPath.trim().isEmpty) { @@ -788,7 +788,7 @@ class _ControlPanelPageState extends State @override Widget build(BuildContext context) { - final core = PmonCore.instance; + final core = InkwatchCore.instance; // 用 Scaffold.appBar 而不是自绘深色容器:状态栏内边距由框架负责, // 不再出现"标题被系统时间压住"(旧自绘 header 的真机缺陷)。 return Scaffold( @@ -809,9 +809,9 @@ class _ControlPanelPageState extends State // 首屏三分之一被它吃掉,而真正要看的指标与折线被挤到下面。 // 现在:标题与平台交给 AppBar(自动处理状态栏内边距),其余信息按重要性重新分配到卡片里; // 口号与能力项降级成一行 caption —— 它们对用户是"看过一次就够"的信息。 - PreferredSizeWidget _appBar(PmonCore core) { + PreferredSizeWidget _appBar(InkwatchCore core) { return AppBar( - titleSpacing: PmonSpace.lg, + titleSpacing: InkwatchSpace.lg, title: Text('墨观 采集端'), actions: [ // 主题切换(最小入口):应用内显式偏好,不跟随系统。 @@ -828,7 +828,7 @@ class _ControlPanelPageState extends State )), ), Padding( - padding: EdgeInsets.only(right: PmonSpace.lg), + padding: EdgeInsets.only(right: InkwatchSpace.lg), child: Center(child: _badge(core.platformName)), ), ], @@ -840,7 +840,7 @@ class _ControlPanelPageState extends State padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: _p.surfaceMuted, - borderRadius: BorderRadius.circular(PmonRadius.pill), + borderRadius: BorderRadius.circular(InkwatchRadius.pill), ), child: Text(text, style: TextStyle( @@ -852,7 +852,7 @@ class _ControlPanelPageState extends State /// 能力一行 caption(原先是深色块里的三个 pill + 一句口号)。 /// 只保留**用户能对应到行为**的三项;"采集核心独立于界面 / 断线按 seq 补传" /// 属实现细节,留在 README 里,不占首屏。 - Widget _capabilityLine(PmonCore core) => Text( + Widget _capabilityLine(InkwatchCore core) => Text( '指标 ${core.metricKeys().length} 项' ' · 后台常驻${core.canRunInBackground ? "支持" : "不支持"}' ' · 守护检查 ${core.guardianCheckIntervalMs ~/ 1000}s', @@ -874,13 +874,13 @@ class _ControlPanelPageState extends State /// —— 那样用户会把"形状变了"误读成"出事了"。 Widget _skeletonScreen(String hint) { return ListView( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), children: [ LoadingHint(text: hint), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), SkeletonMetricGrid(cells: 4), - SizedBox(height: PmonSpace.lg), - SkeletonBox(height: 90, radius: PmonRadius.md), + SizedBox(height: InkwatchSpace.lg), + SkeletonBox(height: 90, radius: InkwatchRadius.md), ], ); } @@ -936,7 +936,7 @@ class _ControlPanelPageState extends State // 是 launchd 拉起的非沙箱进程,读真实家目录那份。用户在下面两项里 // 改地址,只有界面内采集会跟着变,常驻采集不会 —— 不说清的话, // 症状是"改了网关地址、后台那台还在往旧地址发",两边都不报错。 - if (currentPlatform != PmonPlatform.android) ...[ + if (currentPlatform != InkwatchPlatform.android) ...[ Text( '上面两项只对「界面内采集」生效。常驻采集是另一个进程,' '读的是安装器那份配置 —— 装好后在「常驻采集」卡里能看到它' @@ -1060,7 +1060,7 @@ class _ControlPanelPageState extends State /// ② 本版本未接入采集 —— GPU / 温度 / 磁盘 IO(V1.5 路径) /// ③ 其它 —— 兜底提示 Widget _unavailableNote() { - final caps = PmonCore.instance.capabilities(); + final caps = InkwatchCore.instance.capabilities(); if (caps.isEmpty) return SizedBox.shrink(); final missing = caps.entries.where((e) => e.value == false).map((e) => e.key).toSet(); if (missing.isEmpty) return SizedBox.shrink(); @@ -1129,7 +1129,7 @@ class _ControlPanelPageState extends State Widget _configFixNotice() { return Card( child: Padding( - padding: EdgeInsets.all(PmonSpace.md), + padding: EdgeInsets.all(InkwatchSpace.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1160,23 +1160,23 @@ class _ControlPanelPageState extends State } Widget _dashboard(ClientAgent agent) { - final core = PmonCore.instance; + final core = InkwatchCore.instance; return ValueListenableBuilder( valueListenable: _statusVn, builder: (context, st, _) { final f = st.lastFrame ?? const {}; return ListView( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), children: [ if (_configFixes.isNotEmpty) ...[ _configFixNotice(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), ], _statusRow(st, core), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), // 主角:实时指标与折线(首屏就要看到"在采什么") _metricsGrid(f), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), _samplingCard(st), SizedBox(height: 14), // 常驻采集(P0-5):桌面端才有意义 —— 关掉窗口继续采,靠的是 @@ -1221,7 +1221,7 @@ class _ControlPanelPageState extends State hardwareId: _hardwareId, onClaimed: (id) => unawaited(_issueDeviceTokenFor(id)), ), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), // 设备间传文件:发出 / 接收两个区。与账号、配对同级 —— // 它同样是"这台机器与我的其它机器之间"的事,不是采样参数。 // 把采集端会话接进来 → 免去手工粘贴领取码(不通时自动退回粘贴)。 @@ -1232,12 +1232,12 @@ class _ControlPanelPageState extends State false, incomingOffers: _agent?.incomingOffers, ), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), // 解释性内容后置:首屏留给"在采什么","为什么是 —"放到下面 _unavailableNote(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), _counters(st), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), _logPanel(agent), SizedBox(height: 20), ], @@ -1247,7 +1247,7 @@ class _ControlPanelPageState extends State } /// 状态灯三态(方案 13.3:绿在线 / 黄心跳超时疑似离线 / 灰离线) - Widget _statusRow(AgentStatus st, PmonCore core) { + Widget _statusRow(AgentStatus st, InkwatchCore core) { final (color, text) = switch (st.linkState) { LinkState.online => (_p.online, '在线'), LinkState.suspect => (_p.suspect, '疑似离线(心跳超时)'), @@ -1259,7 +1259,7 @@ class _ControlPanelPageState extends State final showHint = !st.connected && st.lastError != null && st.reconnectCount > 0; return Card( child: Padding( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1268,7 +1268,7 @@ class _ControlPanelPageState extends State Row( children: [ _statusDot(color), - SizedBox(width: PmonSpace.md), + SizedBox(width: InkwatchSpace.md), Expanded( child: Text(text, style: TextStyle( @@ -1281,7 +1281,7 @@ class _ControlPanelPageState extends State icon: Icon(Icons.refresh, size: 16), label: Text('强制重连'), ), - SizedBox(width: PmonSpace.xs), + SizedBox(width: InkwatchSpace.xs), TextButton.icon( onPressed: _stop, icon: Icon(Icons.stop_circle_outlined, @@ -1291,18 +1291,18 @@ class _ControlPanelPageState extends State ), ], ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), // 只留"报给谁"这一件用户会去核对的事;device_id 在配置面板的 // 「设备标识」输入框里本来就有,状态卡再显示一遍是重复(信息密度)。 _kvLine('目标', _shortTarget(st.serverUrl)), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), _capabilityLine(core), if (showHint) ...[ - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), Container( - padding: EdgeInsets.all(PmonSpace.md), + padding: EdgeInsets.all(InkwatchSpace.md), decoration: - PmonCard.notice(_p.suspect, _p.suspectSoft), + InkwatchCard.notice(_p.suspect, _p.suspectSoft), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1310,7 +1310,7 @@ class _ControlPanelPageState extends State children: [ Icon(Icons.tips_and_updates_outlined, size: 15, color: _p.suspect), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Expanded( child: Text('连不上网关(已重试 ${st.reconnectCount} 次)', style: TextStyle( @@ -1320,11 +1320,11 @@ class _ControlPanelPageState extends State ), ], ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), Text(st.lastError!, style: TextStyle( fontSize: 12, color: _p.textSecondary)), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), Text( '排查:\n' '· 手机需与服务器在同一局域网;在服务器上执行 ' @@ -1448,12 +1448,12 @@ class _ControlPanelPageState extends State if (f.isEmpty) { final cells = kMetricDisplayCatalog.where((s) => s.primary).length; return Padding( - padding: EdgeInsets.only(bottom: PmonSpace.sm), + padding: EdgeInsets.only(bottom: InkwatchSpace.sm), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LoadingHint(text: '正在采集第一帧…'), - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), SkeletonMetricGrid(cells: cells), ], ), @@ -1479,16 +1479,16 @@ class _ControlPanelPageState extends State crossAxisCount: cols, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - mainAxisSpacing: PmonSpace.md, - crossAxisSpacing: PmonSpace.md, + mainAxisSpacing: InkwatchSpace.md, + crossAxisSpacing: InkwatchSpace.md, // 同上:固定高度,别用宽高比(窄屏会算矮导致文字被裁) mainAxisExtent: cols == 1 ? 66 : 84, children: [ for (final (label, value, key, icon) in cards) Container( padding: EdgeInsets.symmetric( - horizontal: PmonSpace.lg, vertical: PmonSpace.md), - decoration: PmonCard.standard(palette: _p), + horizontal: InkwatchSpace.lg, vertical: InkwatchSpace.md), + decoration: InkwatchCard.standard(palette: _p), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, @@ -1496,7 +1496,7 @@ class _ControlPanelPageState extends State Row( children: [ Icon(icon, size: 15, color: _p.textTertiary), - SizedBox(width: PmonSpace.sm - 2), + SizedBox(width: InkwatchSpace.sm - 2), Expanded( child: Text(label, style: TextStyle( @@ -1507,7 +1507,7 @@ class _ControlPanelPageState extends State ), ], ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, diff --git a/client/lib/ui/account_card.dart b/client/lib/ui/account_card.dart index 3a20bb3..2781e08 100644 --- a/client/lib/ui/account_card.dart +++ b/client/lib/ui/account_card.dart @@ -46,8 +46,8 @@ class AccountCard extends StatefulWidget { } class _AccountCardState extends State { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); late final TextEditingController _emailCtrl; late final TextEditingController _secretCtrl; @@ -670,6 +670,12 @@ class _AccountCardState extends State { Widget _loggedInView() { final info = _info; + // 元信息两行共用同一档样式 —— 它们原先挤在同一行,拆开后必须长得一样, + // 否则「档位比配额重要 / 次要」这种视觉暗示会凭空出现 + final metaStyle = TextStyle( + fontSize: 11.5, + color: _p.textSecondary, + ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -695,12 +701,23 @@ class _AccountCardState extends State { ), if (info != null) ...[ SizedBox(height: 2), - Text( - '${info.email} · 档位 ${info.plan} · 我的设备 ${info.ownedDeviceCount}/${info.deviceQuota} 台', - style: TextStyle( - fontSize: 11.5, - color: _p.textSecondary, + // ── 配额行 ───────────────────────────────────────────── + // **任一数字缺失就整行不渲染**:服务端没给却写「0/0 台」,用户会以为 + // 自己被降级了 —— 而那是编出来的数字(解析层已不再填默认值)。 + if (info.ownedDeviceCount != null && info.deviceQuota != null) + Text( + '我的设备 ${info.ownedDeviceCount}/${info.deviceQuota} 台' + '${info.ownedDeviceCount! >= info.deviceQuota! ? ' · 已达上限' : ''}', + style: metaStyle, ), + // ── 档位行 ───────────────────────────────────────────── + // 中文档位名**只认服务端给的 plan_name**,拿不到就说「未知档位」。 + // ① 绝不回落到机器键字段(free / pro / team):用户看不懂; + // ② 绝不按配额反推档位:free_device_limit 支持实例级覆盖,反推必然算错。 + // 这两条都有源码护栏:test/plan_text_guard_test.dart + Text( + '${info.planName ?? '未知档位'} · 查看三档权益', + style: metaStyle, ), SizedBox(height: 6), Row( diff --git a/client/lib/ui/device_members_card.dart b/client/lib/ui/device_members_card.dart index a0547db..0e7b1df 100644 --- a/client/lib/ui/device_members_card.dart +++ b/client/lib/ui/device_members_card.dart @@ -50,8 +50,8 @@ class DeviceMembersCard extends StatefulWidget { } class _DeviceMembersCardState extends State { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); final _emailCtrl = TextEditingController(); List _members = const []; diff --git a/client/lib/ui/eink_board.dart b/client/lib/ui/eink_board.dart index 6569989..fa394bd 100644 --- a/client/lib/ui/eink_board.dart +++ b/client/lib/ui/eink_board.dart @@ -286,7 +286,11 @@ class EinkBoard extends StatelessWidget { /// 宿主提供的诊断行(刷新间隔、已收帧数、最后刷新时刻…)。 /// MediaQuery 相关的行由看板自己补 —— 那是展示层的事。 - final List<(String, String)> diagnostics; + /// + /// 每行是 `(标签, 值, 值前面的图标?)`。图标单列一个槽位而不是把 `✓` 拼进值: + /// 状态符号一律走 Material Icons —— 字符符号在水墨屏上会渲染成方框, + /// 且无法随主题着色(与 `ui/theme.dart` 那条「样式只许一处定义」同源)。 + final List<(String, String, IconData?)> diagnostics; @override Widget build(BuildContext context) { @@ -296,22 +300,22 @@ class EinkBoard extends StatelessWidget { return Theme( data: einkTheme(), child: Scaffold( - backgroundColor: PmonEink.page, + backgroundColor: InkwatchEink.page, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB( - PmonSpace.lg, PmonSpace.md, PmonSpace.lg, PmonSpace.sm), + InkwatchSpace.lg, InkwatchSpace.md, InkwatchSpace.lg, InkwatchSpace.sm), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _header(), - const SizedBox(height: PmonSpace.sm), + const SizedBox(height: InkwatchSpace.sm), Expanded( child: showDiagnostics ? _diagnostics(context) : (hasData ? _grid(specs) : _noData()), ), - const SizedBox(height: PmonSpace.sm), + const SizedBox(height: InkwatchSpace.sm), _footer(), ], ), @@ -327,18 +331,18 @@ class EinkBoard extends StatelessWidget { /// —— 墨水屏上最省墨、最清楚的分区手段。 Widget _header() { final abnormal = statusKey != 'online'; - final st = statusStyle(statusKey, PmonPalette.eink); + final st = statusStyle(statusKey, InkwatchPalette.eink); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ // 形状而非颜色:实心/半实心/空心/虚线 与 在线/疑似/离线/未知 一一对应 - Text(statusGlyph(statusKey), style: PmonEinkText.glyph), - const SizedBox(width: PmonSpace.sm), + Text(statusGlyph(statusKey), style: InkwatchEinkText.glyph), + const SizedBox(width: InkwatchSpace.sm), Expanded( child: Text(deviceName, - style: PmonEinkText.title, + style: InkwatchEinkText.title, maxLines: 1, overflow: TextOverflow.ellipsis), ), @@ -346,10 +350,10 @@ class EinkBoard extends StatelessWidget { // ● 已经说了在线,右下的刷新时刻已经说了数据在动。 // 只有**异常**才把状态与原因铺出来(那时它们才是有用信息)。 if (abnormal) ...[ - const SizedBox(width: PmonSpace.sm), + const SizedBox(width: InkwatchSpace.sm), Flexible( child: Text('${st.label} · $agoText', - style: PmonEinkText.label, + style: InkwatchEinkText.label, maxLines: 1, overflow: TextOverflow.ellipsis), ), @@ -357,7 +361,7 @@ class EinkBoard extends StatelessWidget { ], ), const SizedBox(height: 6), - const Divider(height: 2, thickness: 2, color: PmonEink.rule), + const Divider(height: 2, thickness: 2, color: InkwatchEink.rule), ], ); } @@ -378,13 +382,13 @@ class EinkBoard extends StatelessWidget { final fixedWidths = parts .map((p) => p.$2.isEmpty ? 0.0 - : unitGap + measureEinkText(' ${p.$2}', PmonEinkText.label).width) + : unitGap + measureEinkText(' ${p.$2}', InkwatchEinkText.label).width) .toList(); // 单格**内容区**(扣掉内边距、标签行与标签下的间距)—— 用来定统一缩放比例 const padH = 12.0; final cellW = c.maxWidth / cols - padH * 2; - final labelH = measureEinkText('内存使用率', PmonEinkText.label).height; + final labelH = measureEinkText('内存使用率', InkwatchEinkText.label).height; // ★ 矮屏要**让位给数值**(2026-09-16 真机暴露): // F50 横屏只有 ~366dp 高,标签 + 内边距吃掉了几乎整格, @@ -403,12 +407,12 @@ class EinkBoard extends StatelessWidget { texts: texts, maxWidth: cellW, maxHeight: cellH, - base: PmonEinkText.value, + base: InkwatchEinkText.value, fixedWidths: fixedWidths, ); // ★ 全屏共用**一个**数值字号(唯一允许改字号的地方,且是算出来的) - final valueStyle = PmonEinkText.value - .copyWith(fontSize: PmonEinkText.value.fontSize! * scale); + final valueStyle = InkwatchEinkText.value + .copyWith(fontSize: InkwatchEinkText.value.fontSize! * scale); final rows2 = []; for (var r = 0; r < rows; r++) { @@ -436,7 +440,7 @@ class EinkBoard extends StatelessWidget { } return Container( decoration: - BoxDecoration(border: Border.all(color: PmonEink.rule, width: 1.5)), + BoxDecoration(border: Border.all(color: InkwatchEink.rule, width: 1.5)), child: Column(children: rows2), ); }); @@ -460,10 +464,10 @@ class EinkBoard extends StatelessWidget { decoration: BoxDecoration( border: Border( right: rightBorder - ? const BorderSide(color: PmonEink.rule, width: 1) + ? const BorderSide(color: InkwatchEink.rule, width: 1) : BorderSide.none, bottom: bottomBorder - ? const BorderSide(color: PmonEink.rule, width: 1) + ? const BorderSide(color: InkwatchEink.rule, width: 1) : BorderSide.none, ), ), @@ -475,7 +479,7 @@ class EinkBoard extends StatelessWidget { // key 只为让测试能**精确定位**数值与标签: // "同屏字号必须一致"这条要求只有在能逐格取值时才可断言。 key: ValueKey('eink-label-${s.key}'), - style: PmonEinkText.label, + style: InkwatchEinkText.label, maxLines: 1, overflow: TextOverflow.ellipsis), SizedBox(height: labelGap), @@ -501,7 +505,7 @@ class EinkBoard extends StatelessWidget { parts.$1, key: ValueKey('eink-value-${s.key}'), style: raw == null - ? valueStyle.copyWith(color: PmonEink.inkSoft) + ? valueStyle.copyWith(color: InkwatchEink.inkSoft) : valueStyle, maxLines: 1, ), @@ -513,8 +517,8 @@ class EinkBoard extends StatelessWidget { child: Text( parts.$2, key: ValueKey('eink-unit-${s.key}'), - style: PmonEinkText.label - .copyWith(color: PmonEink.inkSoft), + style: InkwatchEinkText.label + .copyWith(color: InkwatchEink.inkSoft), maxLines: 1, ), ), @@ -538,17 +542,17 @@ class EinkBoard extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.hourglass_empty, size: 40, color: PmonEink.ink), - const SizedBox(height: PmonSpace.md), - const Text('等待这台设备上报第一帧', style: PmonEinkText.title), - const SizedBox(height: PmonSpace.sm), + const Icon(Icons.hourglass_empty, size: 40, color: InkwatchEink.ink), + const SizedBox(height: InkwatchSpace.md), + const Text('等待这台设备上报第一帧', style: InkwatchEinkText.title), + const SizedBox(height: InkwatchSpace.sm), Text(agoText, - style: PmonEinkText.caption, + style: InkwatchEinkText.caption, textAlign: TextAlign.center, maxLines: 2), if (abnormal) ...[ - const SizedBox(height: PmonSpace.xs), - Text(statusStyle(statusKey, PmonPalette.eink).label, style: PmonEinkText.caption), + const SizedBox(height: InkwatchSpace.xs), + Text(statusStyle(statusKey, InkwatchPalette.eink).label, style: InkwatchEinkText.caption), ], ], ), @@ -569,18 +573,20 @@ class EinkBoard extends StatelessWidget { // ★ 布局尺寸取自**约束**而不是 MediaQuery: // 应用内旋转时 `RotatedBox` 会交换约束,于是两者**不相等** —— // 这正好是最硬的证据(一眼看出"系统没转、是应用自己在转")。 - final rows = <(String, String)>[ - ('窗口(物理)', '${mq.size.width.round()} × ${mq.size.height.round()} dp'), - ('布局(看板用)', '${c.maxWidth.round()} × ${c.maxHeight.round()} dp'), - ('设备像素比', mq.devicePixelRatio.toStringAsFixed(3)), + final rows = <(String, String, IconData?)>[ + ('窗口(物理)', + '${mq.size.width.round()} × ${mq.size.height.round()} dp', null), + ('布局(看板用)', + '${c.maxWidth.round()} × ${c.maxHeight.round()} dp', null), + ('设备像素比', mq.devicePixelRatio.toStringAsFixed(3), null), ('物理像素', '${(mq.size.width * mq.devicePixelRatio).round()} × ' - '${(mq.size.height * mq.devicePixelRatio).round()}'), - ('看板列数', '${einkGridColumns(c.maxWidth, c.maxHeight)} 列'), + '${(mq.size.height * mq.devicePixelRatio).round()}', null), + ('看板列数', '${einkGridColumns(c.maxWidth, c.maxHeight)} 列', null), ...diagnostics, ]; return Container( decoration: - BoxDecoration(border: Border.all(color: PmonEink.rule, width: 1.5)), + BoxDecoration(border: Border.all(color: InkwatchEink.rule, width: 1.5)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -591,7 +597,7 @@ class EinkBoard extends StatelessWidget { decoration: BoxDecoration( border: Border( bottom: i < rows.length - 1 - ? const BorderSide(color: PmonEink.rule, width: 1) + ? const BorderSide(color: InkwatchEink.rule, width: 1) : BorderSide.none, ), ), @@ -599,15 +605,25 @@ class EinkBoard extends StatelessWidget { children: [ Expanded( flex: 2, - child: Text(rows[i].$1, style: PmonEinkText.label), + child: Text(rows[i].$1, style: InkwatchEinkText.label), ), Expanded( flex: 3, - child: Text(rows[i].$2, - style: PmonEinkText.label - .copyWith(color: PmonEink.ink), - maxLines: 1, - overflow: TextOverflow.ellipsis), + child: Row( + children: [ + if (rows[i].$3 != null) ...[ + Icon(rows[i].$3, size: 16, color: InkwatchEink.ink), + const SizedBox(width: 4), + ], + Flexible( + child: Text(rows[i].$2, + style: InkwatchEinkText.label + .copyWith(color: InkwatchEink.ink), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ), + ], + ), ), ], ), @@ -627,31 +643,31 @@ class EinkBoard extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Divider(height: 2, thickness: 2, color: PmonEink.rule), - const SizedBox(height: PmonSpace.sm), + const Divider(height: 2, thickness: 2, color: InkwatchEink.rule), + const SizedBox(height: InkwatchSpace.sm), Row( children: [ Expanded( child: Text('$lastRefreshText · $refreshNote', - style: PmonEinkText.caption, + style: InkwatchEinkText.caption, maxLines: 1, overflow: TextOverflow.ellipsis), ), - const SizedBox(width: PmonSpace.sm), + const SizedBox(width: InkwatchSpace.sm), OutlinedButton(onPressed: onRefresh, child: const Text('刷新')), if (onToggleOrientation != null) ...[ - const SizedBox(width: PmonSpace.sm), + const SizedBox(width: InkwatchSpace.sm), OutlinedButton( onPressed: onToggleOrientation, child: const Text('转屏')), ], if (onToggleDiagnostics != null) ...[ - const SizedBox(width: PmonSpace.xs), + const SizedBox(width: InkwatchSpace.xs), TextButton( onPressed: onToggleDiagnostics, child: Text(showDiagnostics ? '返回' : '自检')), ], if (onExit != null) ...[ - const SizedBox(width: PmonSpace.xs), + const SizedBox(width: InkwatchSpace.xs), TextButton(onPressed: onExit, child: const Text('退出')), ], ], diff --git a/client/lib/ui/eink_mode.dart b/client/lib/ui/eink_mode.dart index 3ae2e37..6549a3e 100644 --- a/client/lib/ui/eink_mode.dart +++ b/client/lib/ui/eink_mode.dart @@ -25,8 +25,8 @@ import 'package:flutter/material.dart'; import 'theme.dart'; /// 全局:当前是否处于水墨屏模式。见文件头(含"必须自己触发重建"的约定)。 -class PmonEinkMode { - PmonEinkMode._(); +class InkwatchEinkMode { + InkwatchEinkMode._(); static bool enabled = false; @@ -36,15 +36,15 @@ class PmonEinkMode { /// 全局:**看板是否正在前台全屏显示**。 /// -/// ── 为什么它必须是"可监听"的,而 [PmonEinkMode] 不必 ────────────── -/// `PmonEinkMode` 只在整棵树重建时被读一次,静态 bool 够用; +/// ── 为什么它必须是"可监听"的,而 [InkwatchEinkMode] 不必 ────────────── +/// `InkwatchEinkMode` 只在整棵树重建时被读一次,静态 bool 够用; /// 但**主壳要据此收起自己的底部导航** —— 看板渲染在 `IndexedStack` 的 body 里, /// 而主壳的 `bottomNavigationBar` 是一个**独立区域**,它不感知 body 里发生了什么。 /// 不发通知的话,副屏底下会一直挂着"采集 / 查看"两个页签(实测反馈)。 /// /// 故这里用 `ValueNotifier`,主壳用 `ValueListenableBuilder` 订阅。 -class PmonEinkBoardMode { - PmonEinkBoardMode._(); +class InkwatchEinkBoardMode { + InkwatchEinkBoardMode._(); static final ValueNotifier active = ValueNotifier(false); @@ -66,14 +66,14 @@ class WaitIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - if (PmonEinkMode.enabled) { + if (InkwatchEinkMode.enabled) { // 静止:实心方块 + 文字。不用 Icon 的 spinner(那些也是动画) return Row( mainAxisSize: MainAxisSize.min, children: [ - Container(width: size * 0.6, height: size * 0.6, color: PmonEink.fill), + Container(width: size * 0.6, height: size * 0.6, color: InkwatchEink.fill), SizedBox(width: 6), - Text(label, style: PmonEinkText.caption), + Text(label, style: InkwatchEinkText.caption), ], ); } @@ -97,13 +97,13 @@ class EinkAwareSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { - final p = PmonPalette.of(context); - if (PmonEinkMode.enabled) { + final p = InkwatchPalette.of(context); + if (InkwatchEinkMode.enabled) { return Container( width: width, height: height, decoration: BoxDecoration( - border: Border.all(color: PmonEink.rule, width: 1), + border: Border.all(color: InkwatchEink.rule, width: 1), ), ); } @@ -112,7 +112,7 @@ class EinkAwareSkeleton extends StatelessWidget { height: height, decoration: BoxDecoration( color: p.surfaceMuted, - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), ), ); } diff --git a/client/lib/ui/pairing_card.dart b/client/lib/ui/pairing_card.dart index 3b768ea..8afcb09 100644 --- a/client/lib/ui/pairing_card.dart +++ b/client/lib/ui/pairing_card.dart @@ -62,7 +62,7 @@ class PairingCard extends StatefulWidget { } class _PairingCardState extends State { - PmonPalette get _p => PmonPalette.of(context); + InkwatchPalette get _p => InkwatchPalette.of(context); late final PairingApi _api; Timer? _tick; diff --git a/client/lib/ui/resident_card.dart b/client/lib/ui/resident_card.dart index b72413a..7a86a1f 100644 --- a/client/lib/ui/resident_card.dart +++ b/client/lib/ui/resident_card.dart @@ -2,12 +2,12 @@ /// /// ── 这张卡要解决什么 ────────────────────────────────────────────── /// 关窗即停:`main.dart` 的 `dispose()` 会 `_agent?.stop()`,而"关掉窗口继续采" -/// 需要常驻进程 + 自启注册。`bin/pmon_agent.dart`(常驻进程)与 -/// `bin/pmon_install.dart`(注册/卸载/状态)早就就绪,但 GUI **一个入口都没有** —— +/// 需要常驻进程 + 自启注册。`bin/inkwatch_agent.dart`(常驻进程)与 +/// `bin/inkwatch_install.dart`(注册/卸载/状态)早就就绪,但 GUI **一个入口都没有** —— /// 用户得自己敲命令,等于这个能力不存在。 /// /// ── 三条设计纪律 ───────────────────────────────────────────────── -/// 1. **判据只有一处**:状态来自 `pmon-install status --json` +/// 1. **判据只有一处**:状态来自 `inkwatch-install status --json` /// (见 `host/resident_service.dart`),本卡不自己 stat 文件、不自己 pgrep。 /// 2. **不在 GUI 里做 launchctl/systemd**:自启产物内容由 Rust 生成、落盘由 /// 非沙箱 CLI 执行(沙箱写不了 LaunchAgents)。GUI 再写一套就是第二套自启逻辑。 @@ -54,8 +54,8 @@ class ResidentCard extends StatefulWidget { } class _ResidentCardState extends State { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); ResidentStatus _st = ResidentStatus.unknown; bool _busy = false; @@ -186,22 +186,22 @@ class _ResidentCardState extends State { return Card( child: Padding( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.memory_outlined, size: 18, color: _p.primary), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text('常驻采集', style: _tx.title), Spacer(), Container( padding: EdgeInsets.symmetric( - horizontal: PmonSpace.sm, vertical: PmonSpace.xs), + horizontal: InkwatchSpace.sm, vertical: InkwatchSpace.xs), decoration: BoxDecoration( color: _p.surfaceMuted, - borderRadius: BorderRadius.circular(PmonRadius.pill), + borderRadius: BorderRadius.circular(InkwatchRadius.pill), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Container( @@ -209,33 +209,33 @@ class _ResidentCardState extends State { height: 8, decoration: BoxDecoration(color: dot, shape: BoxShape.circle), ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text(label, style: _tx.caption.copyWith(color: dot)), ]), ), ], ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), Text( '由系统自启托管:**关掉这个窗口也会继续采集**。' '本卡片只调安装器 CLI 显示与操作,不在这里重写自启逻辑。', style: _tx.caption, ), - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), _statusLine(), if (effectiveConfig != null) ...[ - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), effectiveConfig, ], if (_error != null) ...[ - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), _errorBox(_error!), ], if (_note != null && _error == null) ...[ - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), Text(_note!, style: _tx.caption), ], - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), Row( children: [ if (_st.state != ResidentState.running) @@ -253,7 +253,7 @@ class _ResidentCardState extends State { ? '安装并启动' : '重新安装并启动'), ), - if (_st.state != ResidentState.running) SizedBox(width: PmonSpace.sm), + if (_st.state != ResidentState.running) SizedBox(width: InkwatchSpace.sm), if (_st.state != ResidentState.notInstalled) OutlinedButton( onPressed: _busy @@ -262,7 +262,7 @@ class _ResidentCardState extends State { child: Text('卸载'), ), if (_busy) ...[ - SizedBox(width: PmonSpace.md), + SizedBox(width: InkwatchSpace.md), SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)), ], @@ -278,7 +278,7 @@ class _ResidentCardState extends State { /// /// ── 为什么必须显示它(而不是显示界面自己那份)──────────────────── /// macOS GUI 是**真沙箱**(`com.apple.security.app-sandbox`,`HOME` 指向 - /// `~/Library/Containers/com.pmon.pmonClient/Data`),它读写的 `ConfigStore` + /// `~/Library/Containers/com.inkwatch.inkwatchClient/Data`),它读写的 `ConfigStore` /// 在应用容器里;而常驻采集端由 launchd 拉起(**非沙箱**),读真实家目录。 /// 于是磁盘上**两份 config 同时活着**:用户在界面里改网关地址,只改了容器那份, /// 采集端看到的还是旧的 —— 而两边都不会报错。这比"读不到配置"更隐蔽。 @@ -318,7 +318,7 @@ class _ResidentCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('采集端当前读到:$effective', style: _tx.caption), - SizedBox(height: PmonSpace.xs), + SizedBox(height: InkwatchSpace.xs), if (mismatch) Text( '与上方输入框($wanted)不一致:常驻采集读的是安装器那份配置。' @@ -365,10 +365,10 @@ class _ResidentCardState extends State { Widget _errorBox(String msg) { return Container( width: double.infinity, - padding: EdgeInsets.all(PmonSpace.md), + padding: EdgeInsets.all(InkwatchSpace.md), decoration: BoxDecoration( color: _p.dangerSoft, - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -376,7 +376,7 @@ class _ResidentCardState extends State { Text('操作失败(原因如下,来自安装器输出)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _p.danger)), - SizedBox(height: PmonSpace.xs), + SizedBox(height: InkwatchSpace.xs), // 原样显示 CLI 的失败输出:哪条命令失败、错在哪,都在这里 SelectableText(msg, style: TextStyle( diff --git a/client/lib/ui/skeleton.dart b/client/lib/ui/skeleton.dart index dfa3da7..ccaf69b 100644 --- a/client/lib/ui/skeleton.dart +++ b/client/lib/ui/skeleton.dart @@ -26,7 +26,7 @@ class SkeletonBox extends StatelessWidget { this.width, this.height = 12, // 圆角走既有刻度,不写就地 6:与 viewer_chart 的字号收敛同一纪律 - this.radius = PmonRadius.sm, + this.radius = InkwatchRadius.sm, }); final double? width; @@ -35,7 +35,7 @@ class SkeletonBox extends StatelessWidget { @override Widget build(BuildContext context) { - final p = PmonPalette.of(context); + final p = InkwatchPalette.of(context); return Container( width: width, height: height, @@ -56,18 +56,18 @@ class SkeletonMetricCard extends StatelessWidget { @override Widget build(BuildContext context) { - final p = PmonPalette.of(context); + final p = InkwatchPalette.of(context); return Container( height: height, padding: EdgeInsets.symmetric( - horizontal: PmonSpace.lg, vertical: PmonSpace.md), - decoration: PmonCard.standard(palette: p), + horizontal: InkwatchSpace.lg, vertical: InkwatchSpace.md), + decoration: InkwatchCard.standard(palette: p), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: const [ SkeletonBox(width: 64, height: 11), // 标签位置 - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), SkeletonBox(width: 88, height: 18), // 数值位置 ], ), @@ -93,8 +93,8 @@ class SkeletonMetricGrid extends StatelessWidget { crossAxisCount: cols, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - mainAxisSpacing: PmonSpace.md, - crossAxisSpacing: PmonSpace.md, + mainAxisSpacing: InkwatchSpace.md, + crossAxisSpacing: InkwatchSpace.md, mainAxisExtent: cols == 1 ? 66 : cellHeight, children: List.generate(cells, (_) => SkeletonMetricCard()), ); @@ -110,7 +110,7 @@ class LoadingHint extends StatelessWidget { @override Widget build(BuildContext context) { - final tx = PmonTextStyles.of(PmonPalette.of(context)); + final tx = InkwatchTextStyles.of(InkwatchPalette.of(context)); return Text(text, style: tx.caption); } } diff --git a/client/lib/ui/theme.dart b/client/lib/ui/theme.dart index bf7c649..d14262f 100644 --- a/client/lib/ui/theme.dart +++ b/client/lib/ui/theme.dart @@ -12,7 +12,7 @@ /// /// 1. **颜色/间距/圆角/字号只在这里定义**。页面里出现 `Color(0x…)` 即判红 /// (由 `test/theme_guard_test.dart` 源码结构断言锁定)。 -/// 2. **语义优先于色值**:写 `PmonColors.online` 而不是 `Colors.green` —— +/// 2. **语义优先于色值**:写 `InkwatchColors.online` 而不是 `Colors.green` —— /// 否则"在线"换成别的色时,要满地找哪几处是它。 /// 3. **只提供必要刻度**,不做无限可调的间距体系(4/8/12/16/20/24 够用)。 /// 刻度一多,同一个视觉决策又会有三种写法。 @@ -26,7 +26,7 @@ library; import 'package:flutter/material.dart'; /// 色板与语义色 -abstract final class PmonColors { +abstract final class InkwatchColors { // ── 中性(浅色极简的骨架)────────────────────────────────────── /// 页面底色(比卡片略深,让白卡片浮起来 —— 用明度差代替阴影) static const page = Color(0xFFF6F7F9); @@ -106,7 +106,7 @@ abstract final class PmonColors { /// 颜色 / 状态 / 图表 / 品牌 / 反馈里,凡 Web 专属的交互态与预混实色一律 /// **保持缺省、不写进 Dart**,理由与浅色侧 `kWebOnlyTokens` 完全一致: /// · hover/active/五种 `*-border` 预混实色 → Flutter 用 Material `WidgetState` -/// 处理交互态、用 `withValues(alpha:)` 动态算描边(见 [PmonCard.notice]); +/// 处理交互态、用 `withValues(alpha:)` 动态算描边(见 [InkwatchCard.notice]); /// · `series-2/3/4`、`chart-area/grid/axis`、`*-gap-band/threshold-*` → /// `viewer_chart.dart` 只画单序列,无多序列 / 预混面积需求; /// · `logo-*` 四色 → Dart 侧没有消费点; @@ -115,12 +115,12 @@ abstract final class PmonColors { /// (反向缺项判红),防止有人"好心"补一套用不到的常量。 /// /// 几何 / 排印 / 描边宽度 / 动效 / 端侧派生 / E-Ink 六色**本就不在 dark 块里** -/// (05 §1.4「应保持缺省」),Flutter 已有 `PmonSpace` / `PmonRadius` / -/// `PmonText` 语义档位,一套都不新增。 +/// (05 §1.4「应保持缺省」),Flutter 已有 `InkwatchSpace` / `InkwatchRadius` / +/// `InkwatchText` 语义档位,一套都不新增。 /// /// ⚠️ [textTertiary] 保持 `#8A93A3`(深底 5.02–6.24:1 达标),**显式不同步** /// 浅色点修值 `#646D7D` —— 同步会让深底对比跌到 2.98–3.70:1,制造新违规。 -abstract final class PmonDarkColors { +abstract final class InkwatchDarkColors { // ── 中性面(--iw-page / surface / surface-muted / line)────────── static const page = Color(0xFF0B0E14); static const surface = Color(0xFF151A23); @@ -164,7 +164,7 @@ abstract final class PmonDarkColors { } /// 间距刻度(只此七档;新增刻度前先问"是否真的需要") -abstract final class PmonSpace { +abstract final class InkwatchSpace { static const xs = 4.0; static const sm = 8.0; static const md = 12.0; @@ -174,7 +174,7 @@ abstract final class PmonSpace { } /// 圆角刻度 -abstract final class PmonRadius { +abstract final class InkwatchRadius { static const sm = 8.0; static const md = 12.0; static const lg = 16.0; @@ -182,64 +182,64 @@ abstract final class PmonRadius { } /// 文字样式(三级 + 标题 + 数值) -abstract final class PmonText { +abstract final class InkwatchText { /// 页面/卡片大标题 static const title = TextStyle( - fontSize: 17, fontWeight: FontWeight.w600, color: PmonColors.textPrimary, height: 1.3); + fontSize: 17, fontWeight: FontWeight.w600, color: InkwatchColors.textPrimary, height: 1.3); /// 分区标题(卡片头) static const section = TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: PmonColors.textPrimary, height: 1.35); + fontSize: 14, fontWeight: FontWeight.w600, color: InkwatchColors.textPrimary, height: 1.35); /// 正文 static const body = TextStyle( - fontSize: 13, color: PmonColors.textPrimary, height: 1.5); + fontSize: 13, color: InkwatchColors.textPrimary, height: 1.5); /// 正文(次要) static const bodyMuted = TextStyle( - fontSize: 13, color: PmonColors.textSecondary, height: 1.5); + fontSize: 13, color: InkwatchColors.textSecondary, height: 1.5); /// 说明文字(脚注、解释、诚实边界文案) static const caption = TextStyle( - fontSize: 12, color: PmonColors.textTertiary, height: 1.5); + fontSize: 12, color: InkwatchColors.textTertiary, height: 1.5); /// 图表轴标签 / Tooltip 的最小档(11px)。 /// 与 E-Ink 分支同纪律:界面里不写就地字号,需要小字就走这一档 /// (`viewer_chart.dart` 的 10 / 10.5 / 11.5 全部收敛到此处)。 static const micro = TextStyle( - fontSize: 11, color: PmonColors.textTertiary, height: 1.4); + fontSize: 11, color: InkwatchColors.textTertiary, height: 1.4); /// 指标数值(等宽数字,避免跳动) static const metric = TextStyle( fontSize: 22, fontWeight: FontWeight.w700, - color: PmonColors.textPrimary, + color: InkwatchColors.textPrimary, height: 1.15, fontFeatures: [FontFeature.tabularFigures()]); /// 指标单位/后缀 static const metricUnit = TextStyle( - fontSize: 12, fontWeight: FontWeight.w500, color: PmonColors.textTertiary); + fontSize: 12, fontWeight: FontWeight.w500, color: InkwatchColors.textTertiary); /// 小统计块数值 static const stat = TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: PmonColors.textPrimary, + color: InkwatchColors.textPrimary, fontFeatures: [FontFeature.tabularFigures()]); } /// 卡片与容器装饰(**统一在这里造**,页面不要自己拼 BoxDecoration) -abstract final class PmonCard { +abstract final class InkwatchCard { /// 标准卡片:面 + 发丝线 + 大圆角。不用阴影(极简取向,且阴影在深色/低端屏上更难控) /// - /// [palette] 缺省时回退 [PmonPalette.light](与改造前逐字段一致), - /// 页面自绘一律传 `PmonPalette.of(context)` —— 深色下卡片才真正变深。 - static BoxDecoration standard({Color? tint, PmonPalette? palette}) { - final p = palette ?? PmonPalette.light; + /// [palette] 缺省时回退 [InkwatchPalette.light](与改造前逐字段一致), + /// 页面自绘一律传 `InkwatchPalette.of(context)` —— 深色下卡片才真正变深。 + static BoxDecoration standard({Color? tint, InkwatchPalette? palette}) { + final p = palette ?? InkwatchPalette.light; return BoxDecoration( color: tint ?? p.surface, - borderRadius: BorderRadius.circular(PmonRadius.md), + borderRadius: BorderRadius.circular(InkwatchRadius.md), border: Border.all(color: p.line), ); } @@ -248,16 +248,16 @@ abstract final class PmonCard { /// [base] / [soft] 由调用方按当前色板传入(见 [statusStyle])。 static BoxDecoration notice(Color base, Color soft) => BoxDecoration( color: soft, - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), border: Border.all(color: base.withValues(alpha: 0.28)), ); /// 选中态容器(主色底 + 主色描边) - static BoxDecoration selected({PmonPalette? palette}) { - final p = palette ?? PmonPalette.light; + static BoxDecoration selected({InkwatchPalette? palette}) { + final p = palette ?? InkwatchPalette.light; return BoxDecoration( color: p.primarySoft, - borderRadius: BorderRadius.circular(PmonRadius.md), + borderRadius: BorderRadius.circular(InkwatchRadius.md), border: Border.all(color: p.primary, width: 1.5), ); } @@ -268,26 +268,26 @@ abstract final class PmonCard { /// /// 浅色 / 深色共用**同一段控件装配**([_assemble]),只传入不同的语义色板 /// ([_lightPalette] / [_darkPalette])—— 不另写一套分叉的控件配置。 -/// `buildPmonDarkTheme()` 也走这里,故「只在 theme.dart 内 new ThemeData」的 +/// `buildInkwatchDarkTheme()` 也走这里,故「只在 theme.dart 内 new ThemeData」的 /// 规矩对深色同样成立(页面里就地 `ThemeData(` 由 theme_guard_test.dart 判红)。 -ThemeData buildPmonTheme() => _assemble(_lightPalette, Brightness.light) +ThemeData buildInkwatchTheme() => _assemble(_lightPalette, Brightness.light) .copyWith( - extensions: const >[PmonPalette.light]); + extensions: const >[InkwatchPalette.light]); /// 深色皮肤主题。色值来自 `tokens.css` 的 `html[data-iw-theme="dark"]` -/// (05 §1.4 的 58 个同名键覆写),由 [PmonDarkColors] 逐项转录。 +/// (05 §1.4 的 58 个同名键覆写),由 [InkwatchDarkColors] 逐项转录。 /// -/// 装配结构与 [buildPmonTheme] 完全相同,仅语义色替换为深色板: -/// 圆角 / 间距 / 字号刻度一律沿用 PmonSpace / PmonRadius / PmonText, +/// 装配结构与 [buildInkwatchTheme] 完全相同,仅语义色替换为深色板: +/// 圆角 / 间距 / 字号刻度一律沿用 InkwatchSpace / InkwatchRadius / InkwatchText, /// 不新增任何刻度。E-Ink 看板走独立的 [einkTheme],不受本主题影响。 -ThemeData buildPmonDarkTheme() => _assemble(_darkPalette, Brightness.dark) +ThemeData buildInkwatchDarkTheme() => _assemble(_darkPalette, Brightness.dark) .copyWith( - extensions: const >[PmonPalette.dark]); + extensions: const >[InkwatchPalette.dark]); /// 控件装配用到的语义色子集。**不是新令牌** —— 只是把两套色板喂给同一段装配, /// 避免把 Web 的「预混实色 / 交互态」概念搬进 Dart。 -class _PmonPalette { - const _PmonPalette({ +class _InkwatchPalette { + const _InkwatchPalette({ required this.page, required this.surface, required this.line, @@ -316,39 +316,39 @@ class _PmonPalette { final Color snackbarText; } -const _lightPalette = _PmonPalette( - page: PmonColors.page, - surface: PmonColors.surface, - line: PmonColors.line, - surfaceMuted: PmonColors.surfaceMuted, - textPrimary: PmonColors.textPrimary, - textSecondary: PmonColors.textSecondary, - textTertiary: PmonColors.textTertiary, +const _lightPalette = _InkwatchPalette( + page: InkwatchColors.page, + surface: InkwatchColors.surface, + line: InkwatchColors.line, + surfaceMuted: InkwatchColors.surfaceMuted, + textPrimary: InkwatchColors.textPrimary, + textSecondary: InkwatchColors.textSecondary, + textTertiary: InkwatchColors.textTertiary, textOnAccent: Colors.white, - primary: PmonColors.primary, - primarySoft: PmonColors.primarySoft, + primary: InkwatchColors.primary, + primarySoft: InkwatchColors.primarySoft, // 浅色 Toast:深底白字(--iw-snackbar-bg 浅色 = textPrimary #111827) - snackbarBg: PmonColors.textPrimary, + snackbarBg: InkwatchColors.textPrimary, snackbarText: Colors.white, ); -const _darkPalette = _PmonPalette( - page: PmonDarkColors.page, - surface: PmonDarkColors.surface, - line: PmonDarkColors.line, - surfaceMuted: PmonDarkColors.surfaceMuted, - textPrimary: PmonDarkColors.textPrimary, - textSecondary: PmonDarkColors.textSecondary, - textTertiary: PmonDarkColors.textTertiary, - textOnAccent: PmonDarkColors.textOnAccent, - primary: PmonDarkColors.primary, - primarySoft: PmonDarkColors.primarySoft, +const _darkPalette = _InkwatchPalette( + page: InkwatchDarkColors.page, + surface: InkwatchDarkColors.surface, + line: InkwatchDarkColors.line, + surfaceMuted: InkwatchDarkColors.surfaceMuted, + textPrimary: InkwatchDarkColors.textPrimary, + textSecondary: InkwatchDarkColors.textSecondary, + textTertiary: InkwatchDarkColors.textTertiary, + textOnAccent: InkwatchDarkColors.textOnAccent, + primary: InkwatchDarkColors.primary, + primarySoft: InkwatchDarkColors.primarySoft, // D3 反相:深色 Toast 底 = 浅色 #F2F4F9,文字改用深 surface(15.85:1 AAA) - snackbarBg: PmonDarkColors.snackbarBg, - snackbarText: PmonDarkColors.surface, + snackbarBg: InkwatchDarkColors.snackbarBg, + snackbarText: InkwatchDarkColors.surface, ); -ThemeData _assemble(_PmonPalette p, Brightness brightness) { +ThemeData _assemble(_InkwatchPalette p, Brightness brightness) { final baseScheme = ColorScheme.fromSeed( seedColor: p.primary, brightness: brightness, @@ -356,7 +356,7 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { ); // 深底必须让主色严格落在语义令牌上:fromSeed 会把 primary 推到 tonal 近似值 // (实测 #6E9CFF → #AFC6FF),FilledButton / 选中态就会用到非令牌蓝。 - // 浅色分支保持 fromSeed 原始结果,确保 buildPmonTheme() 逐字段不变。 + // 浅色分支保持 fromSeed 原始结果,确保 buildInkwatchTheme() 逐字段不变。 final colorScheme = brightness == Brightness.dark ? baseScheme.copyWith( primary: p.primary, @@ -380,7 +380,7 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { elevation: 0, scrolledUnderElevation: 0, centerTitle: false, - titleTextStyle: PmonText.title.copyWith(color: p.textPrimary), + titleTextStyle: InkwatchText.title.copyWith(color: p.textPrimary), ), dividerTheme: DividerThemeData(color: p.line, thickness: 1, space: 1), cardTheme: base.cardTheme.copyWith( @@ -389,7 +389,7 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { elevation: 0, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(PmonRadius.md), + borderRadius: BorderRadius.circular(InkwatchRadius.md), side: BorderSide(color: p.line), ), ), @@ -398,18 +398,18 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { filled: true, fillColor: p.surface, contentPadding: const EdgeInsets.symmetric( - horizontal: PmonSpace.md, vertical: PmonSpace.md), - hintStyle: PmonText.caption.copyWith(color: p.textTertiary), + horizontal: InkwatchSpace.md, vertical: InkwatchSpace.md), + hintStyle: InkwatchText.caption.copyWith(color: p.textTertiary), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), borderSide: BorderSide(color: p.line), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), borderSide: BorderSide(color: p.line), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), borderSide: BorderSide(color: p.primary, width: 1.5), ), ), @@ -418,17 +418,17 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { backgroundColor: p.snackbarBg, contentTextStyle: TextStyle(fontSize: 13, color: p.snackbarText), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(PmonRadius.sm)), + borderRadius: BorderRadius.circular(InkwatchRadius.sm)), ), popupMenuTheme: base.popupMenuTheme.copyWith( color: p.surface, surfaceTintColor: Colors.transparent, elevation: 2, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(PmonRadius.sm), + borderRadius: BorderRadius.circular(InkwatchRadius.sm), side: BorderSide(color: p.line), ), - textStyle: PmonText.body.copyWith(color: p.textPrimary), + textStyle: InkwatchText.body.copyWith(color: p.textPrimary), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: p.surface, @@ -472,13 +472,13 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { /// 故收敛成一条函数,新增状态只改这里。 /// /// 只扩到 **4 字段、不带 `border`**:Flutter 的语义描边由 -/// `base.withValues(alpha: 0.28)` 动态生成(见 [PmonCard.notice]), +/// `base.withValues(alpha: 0.28)` 动态生成(见 [InkwatchCard.notice]), /// 没有 Web 端「预混实色描边」这一需求,不要把 CSS 的实现概念搬进 Dart。 -/// [palette] 缺省时回退 [PmonPalette.light](与改造前逐字段一致); +/// [palette] 缺省时回退 [InkwatchPalette.light](与改造前逐字段一致); /// 页面自绘一律传当前色板,深色 / E-Ink 下状态色才跟着换。 ({String label, Color color, Color soft, Color text}) statusStyle( - String key, [PmonPalette? palette]) { - final p = palette ?? PmonPalette.light; + String key, [InkwatchPalette? palette]) { + final p = palette ?? InkwatchPalette.light; switch (key) { case 'online': return (label: '在线', color: p.online, @@ -521,8 +521,8 @@ ThemeData _assemble(_PmonPalette p, Brightness brightness) { /// /// 故墨水屏只用**三档明度**(纯黑 / 深灰 / 纯白),分层靠**字重与描边**而不是底色, /// 状态靠**形状**而不是颜色(见 [statusGlyph])。 -class PmonEink { - PmonEink._(); +class InkwatchEink { + InkwatchEink._(); /// 底:纯白。墨水屏的"原生"是白纸,浅灰底只会浪费对比度 static const page = Color(0xFFFFFFFF); @@ -555,33 +555,33 @@ class PmonEink { /// /// 为什么全部加粗(最低 w600):墨水屏墨滴扩散会让细笔画发虚, /// w400 正文在 6″ 300PPI 上远看会断笔。 -class PmonEinkText { - PmonEinkText._(); +class InkwatchEinkText { + InkwatchEinkText._(); /// 数值(**基准字号**)。实际字号 = 本档 × `einkUniformValueScale()` /// 算出的全屏统一比例(只会缩、不会放)。 static const value = TextStyle( - fontSize: 40, fontWeight: FontWeight.w700, color: PmonEink.ink, height: 1.0); + fontSize: 40, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.0); /// 状态形状(●/◐/○/◌)—— 与设备名同行,比它略大以便一眼可辨 static const glyph = TextStyle( - fontSize: 22, fontWeight: FontWeight.w700, color: PmonEink.ink, height: 1.0); + fontSize: 22, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.0); /// 设备名 static const title = TextStyle( - fontSize: 20, fontWeight: FontWeight.w700, color: PmonEink.ink, height: 1.15); + fontSize: 20, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.15); /// 指标标签 / 按钮文字(同号,语义不同则用字重区分,不靠字号) static const label = TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: PmonEink.inkSoft, height: 1.2); + fontSize: 14, fontWeight: FontWeight.w600, color: InkwatchEink.inkSoft, height: 1.2); /// 按钮文字:与 [label] **同号**,只提字重与颜色(不靠改字号来表达重要) static const action = TextStyle( - fontSize: 14, fontWeight: FontWeight.w700, color: PmonEink.ink, height: 1.2); + fontSize: 14, fontWeight: FontWeight.w700, color: InkwatchEink.ink, height: 1.2); /// 角落说明(刷新时刻、异常时的末帧提示) static const caption = TextStyle( - fontSize: 12, fontWeight: FontWeight.w600, color: PmonEink.inkSoft, height: 1.3); + fontSize: 12, fontWeight: FontWeight.w600, color: InkwatchEink.inkSoft, height: 1.3); } /// 状态 → **形状**的唯一映射(水墨屏用)。 @@ -616,21 +616,21 @@ String statusGlyph(String key) { /// ── 为什么需要它(整屏深色化)──────────────────────────────────── /// [ThemeData] 只负责系统控件(输入框 / 菜单 / SnackBar / 对话框…), /// 而本项目的卡片、状态灯、说明文字是**自绘**的(BoxDecoration / TextStyle / -/// Icon color)。它们过去直接引用 [PmonColors] 静态常量,于是深色下 +/// Icon color)。它们过去直接引用 [InkwatchColors] 静态常量,于是深色下 /// 「系统控件变深了、卡片和文字仍是浅色」—— 整屏并没有真正深色。 /// /// 故把页面自绘会消费的全部语义色收进一份 [ThemeExtension], -/// 由 [buildPmonTheme] / [buildPmonDarkTheme] / [einkTheme] 各挂一份实例。 -/// 页面统一用 [PmonPalette.of] 取当前色板: -/// · [light] 逐项等于 [PmonColors] —— 浅色视觉一字不变; -/// · [dark] 逐项等于 [PmonDarkColors] —— 深色整屏生效; -/// · [eink] 全部映射到 [PmonEink] 六色 —— 纯黑白、无彩色。 +/// 由 [buildInkwatchTheme] / [buildInkwatchDarkTheme] / [einkTheme] 各挂一份实例。 +/// 页面统一用 [InkwatchPalette.of] 取当前色板: +/// · [light] 逐项等于 [InkwatchColors] —— 浅色视觉一字不变; +/// · [dark] 逐项等于 [InkwatchDarkColors] —— 深色整屏生效; +/// · [eink] 全部映射到 [InkwatchEink] 六色 —— 纯黑白、无彩色。 /// -/// 色值**定义源**仍是 [PmonColors] / [PmonDarkColors] / [PmonEink] 静态常量 +/// 色值**定义源**仍是 [InkwatchColors] / [InkwatchDarkColors] / [InkwatchEink] 静态常量 /// (parity / 护栏测试依赖,一个不动);本类只做「按主题选取」,不新增色值、 /// 不新增圆角 / 间距 / 字号刻度。 -class PmonPalette extends ThemeExtension { - const PmonPalette({ +class InkwatchPalette extends ThemeExtension { + const InkwatchPalette({ required this.page, required this.surface, required this.surfaceMuted, @@ -684,96 +684,96 @@ class PmonPalette extends ThemeExtension { /// 当前主题的色板。裸 MaterialApp(widget 测试未挂 extensions)回退 [light], /// 与改造前视觉一致,也不会因 extension 缺失而崩。 - static PmonPalette of(BuildContext context) => - Theme.of(context).extension() ?? light; + static InkwatchPalette of(BuildContext context) => + Theme.of(context).extension() ?? light; - /// 浅色:逐项 = [PmonColors]。 - static const light = PmonPalette( - page: PmonColors.page, - surface: PmonColors.surface, - surfaceMuted: PmonColors.surfaceMuted, - line: PmonColors.line, - textPrimary: PmonColors.textPrimary, - textSecondary: PmonColors.textSecondary, - textTertiary: PmonColors.textTertiary, - primary: PmonColors.primary, - primarySoft: PmonColors.primarySoft, - online: PmonColors.online, - onlineSoft: PmonColors.onlineSoft, - onlineText: PmonColors.onlineText, - suspect: PmonColors.suspect, - suspectSoft: PmonColors.suspectSoft, - suspectText: PmonColors.suspectText, - offline: PmonColors.offline, - offlineText: PmonColors.offlineText, - unknownText: PmonColors.unknownText, - danger: PmonColors.danger, - dangerSoft: PmonColors.dangerSoft, - dangerText: PmonColors.dangerText, - infoText: PmonColors.infoText, - chartLine: PmonColors.chartLine, - gap: PmonColors.gap, + /// 浅色:逐项 = [InkwatchColors]。 + static const light = InkwatchPalette( + page: InkwatchColors.page, + surface: InkwatchColors.surface, + surfaceMuted: InkwatchColors.surfaceMuted, + line: InkwatchColors.line, + textPrimary: InkwatchColors.textPrimary, + textSecondary: InkwatchColors.textSecondary, + textTertiary: InkwatchColors.textTertiary, + primary: InkwatchColors.primary, + primarySoft: InkwatchColors.primarySoft, + online: InkwatchColors.online, + onlineSoft: InkwatchColors.onlineSoft, + onlineText: InkwatchColors.onlineText, + suspect: InkwatchColors.suspect, + suspectSoft: InkwatchColors.suspectSoft, + suspectText: InkwatchColors.suspectText, + offline: InkwatchColors.offline, + offlineText: InkwatchColors.offlineText, + unknownText: InkwatchColors.unknownText, + danger: InkwatchColors.danger, + dangerSoft: InkwatchColors.dangerSoft, + dangerText: InkwatchColors.dangerText, + infoText: InkwatchColors.infoText, + chartLine: InkwatchColors.chartLine, + gap: InkwatchColors.gap, ); - /// 深色:逐项 = [PmonDarkColors]。 - static const dark = PmonPalette( - page: PmonDarkColors.page, - surface: PmonDarkColors.surface, - surfaceMuted: PmonDarkColors.surfaceMuted, - line: PmonDarkColors.line, - textPrimary: PmonDarkColors.textPrimary, - textSecondary: PmonDarkColors.textSecondary, - textTertiary: PmonDarkColors.textTertiary, - primary: PmonDarkColors.primary, - primarySoft: PmonDarkColors.primarySoft, - online: PmonDarkColors.online, - onlineSoft: PmonDarkColors.onlineSoft, - onlineText: PmonDarkColors.onlineText, - suspect: PmonDarkColors.suspect, - suspectSoft: PmonDarkColors.suspectSoft, - suspectText: PmonDarkColors.suspectText, - offline: PmonDarkColors.offline, - offlineText: PmonDarkColors.offlineText, - unknownText: PmonDarkColors.unknownText, - danger: PmonDarkColors.danger, - dangerSoft: PmonDarkColors.dangerSoft, - dangerText: PmonDarkColors.dangerText, - infoText: PmonDarkColors.infoText, - chartLine: PmonDarkColors.chartLine, - gap: PmonDarkColors.gap, + /// 深色:逐项 = [InkwatchDarkColors]。 + static const dark = InkwatchPalette( + page: InkwatchDarkColors.page, + surface: InkwatchDarkColors.surface, + surfaceMuted: InkwatchDarkColors.surfaceMuted, + line: InkwatchDarkColors.line, + textPrimary: InkwatchDarkColors.textPrimary, + textSecondary: InkwatchDarkColors.textSecondary, + textTertiary: InkwatchDarkColors.textTertiary, + primary: InkwatchDarkColors.primary, + primarySoft: InkwatchDarkColors.primarySoft, + online: InkwatchDarkColors.online, + onlineSoft: InkwatchDarkColors.onlineSoft, + onlineText: InkwatchDarkColors.onlineText, + suspect: InkwatchDarkColors.suspect, + suspectSoft: InkwatchDarkColors.suspectSoft, + suspectText: InkwatchDarkColors.suspectText, + offline: InkwatchDarkColors.offline, + offlineText: InkwatchDarkColors.offlineText, + unknownText: InkwatchDarkColors.unknownText, + danger: InkwatchDarkColors.danger, + dangerSoft: InkwatchDarkColors.dangerSoft, + dangerText: InkwatchDarkColors.dangerText, + infoText: InkwatchDarkColors.infoText, + chartLine: InkwatchDarkColors.chartLine, + gap: InkwatchDarkColors.gap, ); - /// 水墨屏:全部映射到 [PmonEink] 六色(白 / 黑 / 深灰,R=G=B 无彩色)。 + /// 水墨屏:全部映射到 [InkwatchEink] 六色(白 / 黑 / 深灰,R=G=B 无彩色)。 /// 看板状态靠字形([statusGlyph])而非颜色,此处只需保证绝无彩色。 - static const eink = PmonPalette( - page: PmonEink.page, - surface: PmonEink.box, - surfaceMuted: PmonEink.page, - line: PmonEink.rule, - textPrimary: PmonEink.ink, - textSecondary: PmonEink.inkSoft, - textTertiary: PmonEink.inkSoft, - primary: PmonEink.ink, - primarySoft: PmonEink.page, - online: PmonEink.ink, - onlineSoft: PmonEink.page, - onlineText: PmonEink.ink, - suspect: PmonEink.ink, - suspectSoft: PmonEink.page, - suspectText: PmonEink.ink, - offline: PmonEink.inkSoft, - offlineText: PmonEink.inkSoft, - unknownText: PmonEink.inkSoft, - danger: PmonEink.ink, - dangerSoft: PmonEink.page, - dangerText: PmonEink.ink, - infoText: PmonEink.ink, - chartLine: PmonEink.ink, - gap: PmonEink.inkSoft, + static const eink = InkwatchPalette( + page: InkwatchEink.page, + surface: InkwatchEink.box, + surfaceMuted: InkwatchEink.page, + line: InkwatchEink.rule, + textPrimary: InkwatchEink.ink, + textSecondary: InkwatchEink.inkSoft, + textTertiary: InkwatchEink.inkSoft, + primary: InkwatchEink.ink, + primarySoft: InkwatchEink.page, + online: InkwatchEink.ink, + onlineSoft: InkwatchEink.page, + onlineText: InkwatchEink.ink, + suspect: InkwatchEink.ink, + suspectSoft: InkwatchEink.page, + suspectText: InkwatchEink.ink, + offline: InkwatchEink.inkSoft, + offlineText: InkwatchEink.inkSoft, + unknownText: InkwatchEink.inkSoft, + danger: InkwatchEink.ink, + dangerSoft: InkwatchEink.page, + dangerText: InkwatchEink.ink, + infoText: InkwatchEink.ink, + chartLine: InkwatchEink.ink, + gap: InkwatchEink.inkSoft, ); @override - PmonPalette copyWith({ + InkwatchPalette copyWith({ Color? page, Color? surface, Color? surfaceMuted, @@ -799,7 +799,7 @@ class PmonPalette extends ThemeExtension { Color? chartLine, Color? gap, }) => - PmonPalette( + InkwatchPalette( page: page ?? this.page, surface: surface ?? this.surface, surfaceMuted: surfaceMuted ?? this.surfaceMuted, @@ -827,9 +827,9 @@ class PmonPalette extends ThemeExtension { ); @override - PmonPalette lerp(covariant PmonPalette? other, double t) { + InkwatchPalette lerp(covariant InkwatchPalette? other, double t) { if (other == null) return this; - return PmonPalette( + return InkwatchPalette( page: Color.lerp(page, other.page, t)!, surface: Color.lerp(surface, other.surface, t)!, surfaceMuted: Color.lerp(surfaceMuted, other.surfaceMuted, t)!, @@ -858,29 +858,29 @@ class PmonPalette extends ThemeExtension { } } -/// 文字样式出口:把 [PmonText] 的档位(含浅色 color)按当前色板覆盖文字色。 +/// 文字样式出口:把 [InkwatchText] 的档位(含浅色 color)按当前色板覆盖文字色。 /// -/// 页面自绘统一走 `PmonTextStyles.of(p).caption`,避免在几十处散写 -/// `PmonText.caption.copyWith(color: ...)` —— 那是同一份映射散落多处。 -/// [PmonText] 静态常量本身**保留不动**(护栏 / parity 测试依赖它)。 -class PmonTextStyles { - const PmonTextStyles(this.p); +/// 页面自绘统一走 `InkwatchTextStyles.of(p).caption`,避免在几十处散写 +/// `InkwatchText.caption.copyWith(color: ...)` —— 那是同一份映射散落多处。 +/// [InkwatchText] 静态常量本身**保留不动**(护栏 / parity 测试依赖它)。 +class InkwatchTextStyles { + const InkwatchTextStyles(this.p); - final PmonPalette p; + final InkwatchPalette p; - static PmonTextStyles of(PmonPalette p) => PmonTextStyles(p); + static InkwatchTextStyles of(InkwatchPalette p) => InkwatchTextStyles(p); - TextStyle get title => PmonText.title.copyWith(color: p.textPrimary); - TextStyle get section => PmonText.section.copyWith(color: p.textPrimary); - TextStyle get body => PmonText.body.copyWith(color: p.textPrimary); + TextStyle get title => InkwatchText.title.copyWith(color: p.textPrimary); + TextStyle get section => InkwatchText.section.copyWith(color: p.textPrimary); + TextStyle get body => InkwatchText.body.copyWith(color: p.textPrimary); TextStyle get bodyMuted => - PmonText.bodyMuted.copyWith(color: p.textSecondary); - TextStyle get caption => PmonText.caption.copyWith(color: p.textTertiary); - TextStyle get micro => PmonText.micro.copyWith(color: p.textTertiary); - TextStyle get metric => PmonText.metric.copyWith(color: p.textPrimary); + InkwatchText.bodyMuted.copyWith(color: p.textSecondary); + TextStyle get caption => InkwatchText.caption.copyWith(color: p.textTertiary); + TextStyle get micro => InkwatchText.micro.copyWith(color: p.textTertiary); + TextStyle get metric => InkwatchText.metric.copyWith(color: p.textPrimary); TextStyle get metricUnit => - PmonText.metricUnit.copyWith(color: p.textTertiary); - TextStyle get stat => PmonText.stat.copyWith(color: p.textPrimary); + InkwatchText.metricUnit.copyWith(color: p.textTertiary); + TextStyle get stat => InkwatchText.stat.copyWith(color: p.textPrimary); } /// 水墨屏专用 Material 主题。 @@ -888,64 +888,64 @@ class PmonTextStyles { /// 与彩色版的三点不同,都是为了**减少刷新**: /// 1. `splashFactory: NoSplash` —— 去掉点击水波纹(那是每帧重绘的动画); /// 2. 全部 `elevation: 0` —— 阴影在墨水屏上只会变成一圈脏灰; -/// 3. 颜色走 [PmonEink] 三档明度。 +/// 3. 颜色走 [InkwatchEink] 三档明度。 /// /// **只在 `theme.dart` 里 new ThemeData**(与彩色版同一条规矩)—— /// 页面里就地 `ThemeData(` 会被 `test/theme_guard_test.dart` 判红。 /// -/// 挂 [PmonPalette.eink] 是必须的:看板内部若改用 [PmonPalette.of] 查询色板, +/// 挂 [InkwatchPalette.eink] 是必须的:看板内部若改用 [InkwatchPalette.of] 查询色板, /// 缺了这一份会取到 null 而崩。 ThemeData einkTheme() => ThemeData( useMaterial3: true, - extensions: const >[PmonPalette.eink], - scaffoldBackgroundColor: PmonEink.page, + extensions: const >[InkwatchPalette.eink], + scaffoldBackgroundColor: InkwatchEink.page, colorScheme: ColorScheme.fromSeed( - seedColor: PmonEink.ink, + seedColor: InkwatchEink.ink, brightness: Brightness.light, ).copyWith( - surface: PmonEink.page, - onSurface: PmonEink.ink, - primary: PmonEink.ink, - onPrimary: PmonEink.page, + surface: InkwatchEink.page, + onSurface: InkwatchEink.ink, + primary: InkwatchEink.ink, + onPrimary: InkwatchEink.page, ), splashFactory: NoSplash.splashFactory, splashColor: const Color(0x00000000), highlightColor: const Color(0x00000000), appBarTheme: const AppBarTheme( - backgroundColor: PmonEink.page, - foregroundColor: PmonEink.ink, + backgroundColor: InkwatchEink.page, + foregroundColor: InkwatchEink.ink, elevation: 0, scrolledUnderElevation: 0, centerTitle: false, ), cardTheme: const CardThemeData( - color: PmonEink.box, + color: InkwatchEink.box, elevation: 0, margin: EdgeInsets.zero, ), dividerTheme: const DividerThemeData( - color: PmonEink.rule, + color: InkwatchEink.rule, thickness: 1, space: 1, ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: PmonEink.ink, - side: const BorderSide(color: PmonEink.rule, width: 1.5), + foregroundColor: InkwatchEink.ink, + side: const BorderSide(color: InkwatchEink.rule, width: 1.5), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - textStyle: PmonEinkText.action, + textStyle: InkwatchEinkText.action, ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - foregroundColor: PmonEink.ink, - textStyle: PmonEinkText.action, + foregroundColor: InkwatchEink.ink, + textStyle: InkwatchEinkText.action, ), ), snackBarTheme: SnackBarThemeData( - backgroundColor: PmonEink.ink, - contentTextStyle: PmonEinkText.label.copyWith(color: PmonEink.page), + backgroundColor: InkwatchEink.ink, + contentTextStyle: InkwatchEinkText.label.copyWith(color: InkwatchEink.page), behavior: SnackBarBehavior.floating, elevation: 0, ), diff --git a/client/lib/ui/transfer_card.dart b/client/lib/ui/transfer_card.dart index 6870547..f8e63a5 100644 --- a/client/lib/ui/transfer_card.dart +++ b/client/lib/ui/transfer_card.dart @@ -67,8 +67,8 @@ class FileTransferCard extends StatefulWidget { } class _FileTransferCardState extends State { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); // ── 发出侧 ── LanOutgoing? _offer; @@ -81,7 +81,11 @@ class _FileTransferCardState extends State { /// 「已通过服务器通知对端」或「没通知成,请把码发过去」—— /// **这两句话必须分开说**:前者用户什么都不用做,后者要他去转发。 - String? _offerHint; + /// + /// 状态图标单独一槽(`icon` + `tint`),不拼进文本: + /// Unicode 的 `✓`/`⚠` 在水墨屏上会渲染成方框,且无法随主题着色; + /// `test/symbol_guard_test.dart` 按码位扫 lib/ 防止这类写法回归。 + ({IconData? icon, Color? tint, String text})? _offerHint; // ── 免粘贴:对端主动送来的待领文件 ── StreamSubscription? _offerSub; @@ -219,12 +223,25 @@ class _FileTransferCardState extends State { _code = encodeLanTicket( endpoint: s.endpoint ?? '中继', token: o.token, name: o.name, size: o.size); _offerHint = !pushed - ? '没能通知对端,也开不了中继 —— 请把下面的码发给它。' + ? ( + icon: Icons.warning_amber, + tint: _p.suspect, + text: '没能通知对端,也开不了中继 —— 请把下面的码发给它。', + ) : viaRelay - ? '⚠️ 已走**中继**(两台设备不在同一局域网)。' - '文件经服务器中转、不落盘,但会消耗流量包;' - '对方点「收下」后开始传输(限速 ${_fmtBps(relayBps)})。' - : '✓ 已走**局域网直连**(最快、且不消耗服务器流量)。对端会直接出现待领提示。'; + ? ( + icon: Icons.warning_amber, + tint: _p.suspect, + text: '已走**中继**(两台设备不在同一局域网)。' + '文件经服务器中转、不落盘,但会消耗流量包;' + '对方点「收下」后开始传输(限速 ${_fmtBps(relayBps)})。', + ) + : ( + icon: Icons.check_circle, + tint: _p.online, + text: '已走**局域网直连**(最快、且不消耗服务器流量)。' + '对端会直接出现待领提示。', + ); _sendBusy = false; }); } catch (e) { @@ -416,7 +433,7 @@ class _FileTransferCardState extends State { Text(_sendBusy ? '处理中…' : '选择文件'), ]), ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), // ⚠️ 这句话必须在**点发送之前**就摆在界面上:跨网只能走服务器中继, // 而中继当前有 1 MiB 单次上限。等到进度条不动再说,等于骗人。 Text('跨网(两台设备不在同一局域网)时走服务器中继,' @@ -429,9 +446,26 @@ class _FileTransferCardState extends State { overflow: TextOverflow.ellipsis), SizedBox(height: 6), if (_offerHint != null) ...[ - Text(_offerHint!, - style: TextStyle( - fontSize: 11.5, color: _p.textSecondary, height: 1.4)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_offerHint!.icon != null) ...[ + Padding( + padding: const EdgeInsets.only(top: 1.5), + child: Icon(_offerHint!.icon, + size: 14, color: _offerHint!.tint), + ), + const SizedBox(width: 4), + ], + Expanded( + child: Text(_offerHint!.text, + style: TextStyle( + fontSize: 11.5, + color: _p.textSecondary, + height: 1.4)), + ), + ], + ), SizedBox(height: 6), ], // 中继上传进度。**只在真走中继时出现** —— 直连那条路是秒传的, @@ -612,16 +646,28 @@ class _FileTransferCardState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _result!.verified - ? '✓ 已收下并校验通过' - : '⚠ 已收下但校验未通过', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: _result!.verified - ? _p.online - : _p.danger), + Row( + children: [ + // 状态图标走 Material Icons,不拼进文本: + // 字符符号在水墨屏上渲染成方框,也无法随主题着色 + Icon( + _result!.verified + ? Icons.check_circle + : Icons.error_outline, + size: 15, + color: _result!.verified ? _p.online : _p.danger, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + _result!.verified ? '已收下并校验通过' : '已收下但校验未通过', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _result!.verified ? _p.online : _p.danger), + ), + ), + ], ), SizedBox(height: 4), Text('${fmtSize(_result!.bytes)} 保存于 ${_result!.path}', diff --git a/client/lib/ui/viewer_chart.dart b/client/lib/ui/viewer_chart.dart index 8df68ef..df98341 100644 --- a/client/lib/ui/viewer_chart.dart +++ b/client/lib/ui/viewer_chart.dart @@ -37,8 +37,8 @@ class ViewerChart extends StatelessWidget { @override Widget build(BuildContext context) { - final p = PmonPalette.of(context); - final tx = PmonTextStyles.of(p); + final p = InkwatchPalette.of(context); + final tx = InkwatchTextStyles.of(p); final pts = _numericPoints(); if (pts.isEmpty) { return SizedBox(height: height, child: _emptyHint(p, tx)); @@ -156,7 +156,7 @@ class ViewerChart extends StatelessWidget { return '${v.toStringAsFixed(spec?.digits == 0 ? 0 : 1)}${spec?.unit ?? ''}'; } - Widget _emptyHint(PmonPalette p, PmonTextStyles tx) { + Widget _emptyHint(InkwatchPalette p, InkwatchTextStyles tx) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/client/lib/ui/viewer_page.dart b/client/lib/ui/viewer_page.dart index 5acd7eb..df63297 100644 --- a/client/lib/ui/viewer_page.dart +++ b/client/lib/ui/viewer_page.dart @@ -88,8 +88,8 @@ class ViewerPage extends StatefulWidget { } class _ViewerPageState extends State with WidgetsBindingObserver { - PmonPalette get _p => PmonPalette.of(context); - PmonTextStyles get _tx => PmonTextStyles.of(_p); + InkwatchPalette get _p => InkwatchPalette.of(context); + InkwatchTextStyles get _tx => InkwatchTextStyles.of(_p); ViewerSession? _session; AppConfig _cfg = AppConfig(); @@ -106,7 +106,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // ① 大字段版面(`ui/eink_board.dart`)—— 6 大格、一屏放下、状态用形状; // ② **刷新节流**(下面的 `_einkTimer`)—— 数据每秒到,但只在间隔到点才重建 // (每次重建 = 一次屏刷新,墨水屏的成本全在这里); - // ③ 全局零动画(`PmonEinkMode.enabled`)—— 转圈/呼吸/水波纹全部静止。 + // ③ 全局零动画(`InkwatchEinkMode.enabled`)—— 转圈/呼吸/水波纹全部静止。 bool _eink = false; int _einkSec = 10; Timer? _einkTimer; @@ -221,7 +221,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { unawaited(AndroidForeground.setKeepScreenOn(false)); _applyEinkOrientation(null); // 系统栏与主壳导航都要还回去,否则离开这一页会留下"没有导航栏"的副作用 - PmonEinkBoardMode.active.value = false; + InkwatchEinkBoardMode.active.value = false; unawaited( SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, @@ -492,7 +492,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { /// 前者全版本可用,且语义就是"把系统栏都还回来"。 void _syncBoardChrome() { final boardMode = _eink && !_einkPaused; - PmonEinkBoardMode.active.value = boardMode; + InkwatchEinkBoardMode.active.value = boardMode; unawaited( boardMode ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) @@ -511,17 +511,24 @@ class _ViewerPageState extends State with WidgetsBindingObserver { /// ⚠️ 探测结果**不是实时推送的**:它在下一次节流刷新时反映到界面(最多 10 秒)。 /// 这是刻意的取舍 —— 为一个诊断行加一条推送通道要多一处要维护的接口, /// 而看板本来就有节流刷新,够用。 - String _lanSummary() { + /// 返回 `(icon, text)` 而不是拼好前缀的字符串 —— 状态图标一律走 Material Icons。 + /// 水墨屏上 Unicode 字形与 emoji 会渲染成方框,且无法随主题着色; + /// `test/symbol_guard_test.dart` 按码位扫 lib/ 来防止回归。 + ({IconData? icon, String text}) _lanSummary() { final s = _session; - if (s == null) return '无会话'; - if (s.lanProbing) return '检测中…'; + if (s == null) return (icon: null, text: '无会话'); + if (s.lanProbing) return (icon: null, text: '检测中…'); final hit = s.lanReachable; // 「可达」与「正在走」是两件事:探测通了但还没切过去(或在切换路上) // 都要能看出来,否则用户无法判断省流量到底生效没有。 - if (hit != null && s.lanMode) return '✓ 走内网 · $hit'; - if (hit != null) return '可达(未切换)· $hit'; - if (s.lanEndpoints.isEmpty) return '对端未上报(可能不在同一局域网)'; - return '不可达 · 已试 ${s.lanEndpoints.join('、')}'; + if (hit != null && s.lanMode) { + return (icon: Icons.check_circle, text: '走内网 · $hit'); + } + if (hit != null) return (icon: null, text: '可达(未切换)· $hit'); + if (s.lanEndpoints.isEmpty) { + return (icon: null, text: '对端未上报(可能不在同一局域网)'); + } + return (icon: null, text: '不可达 · 已试 ${s.lanEndpoints.join('、')}'); } /// 看板外壳:必要时套一层**应用内旋转**。 @@ -590,7 +597,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { /// 配置里的水墨屏设置 → 运行时。**唯一的应用点**(启动与用户切换都走它)。 void _applyEink(AppConfig cfg) { final on = cfg.einkMode; - PmonEinkMode.enabled = on; + InkwatchEinkMode.enabled = on; setState(() { _eink = on; _einkSec = cfg.einkRefreshSec; @@ -743,12 +750,12 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // 被系统时间压住)。链路状态放在 AppBar 右侧 —— 用户瞄一眼就知道"通不通"。 return Scaffold( appBar: AppBar( - titleSpacing: PmonSpace.lg, + titleSpacing: InkwatchSpace.lg, title: Text('墨观 查看端'), actions: [ Center(child: _linkChip()), _einkMenu(), - SizedBox(width: PmonSpace.xs), + SizedBox(width: InkwatchSpace.xs), ], ), body: _body(), @@ -856,6 +863,8 @@ class _ViewerPageState extends State with WidgetsBindingObserver { _st.activeDeviceName ?? (_series.deviceId.isEmpty ? '未选择设备' : _series.deviceId); + final lan = _lanSummary(); + return EinkBoard( deviceName: name, statusKey: statusKey, @@ -873,8 +882,10 @@ class _ViewerPageState extends State with WidgetsBindingObserver { }), showDiagnostics: _einkDiag, onToggleDiagnostics: () => setState(() => _einkDiag = !_einkDiag), + // 每行是 (标签, 值, 值前面的图标?) —— 图标单独一槽,符号不拼进文本 + // (Material Icons 才能随主题着色;字符符号在水墨屏上会渲染成方框) diagnostics: [ - ('转屏方式', _softRotate ? '应用内(请把设备横置)' : '系统'), + ('转屏方式', _softRotate ? '应用内(请把设备横置)' : '系统', null), // 局域网底座的状态。**"没开"和"开了但对面连不上"必须能分辨** —— // 前者是 lastError,后者表现为"地址与端口都在、就是连不上"。 ( @@ -882,13 +893,14 @@ class _ViewerPageState extends State with WidgetsBindingObserver { LanService.instance.running ? '已开 · ${LanService.instance.endpoint}' : (LanService.instance.lastError?.toString() ?? '未启动'), + null, ), - ('对端内网', _lanSummary()), - ('刷新间隔', _einkSec == 0 ? '仅手动' : '$_einkSec 秒'), - ('已收帧数', '${_series.received}'), - ('本会话断点', '${_series.gapCount}'), - ('最后刷新', _einkAt), - ('常驻内存', '${(ProcessInfo.currentRss / 1024 / 1024).round()} MB'), + ('对端内网', lan.text, lan.icon), + ('刷新间隔', _einkSec == 0 ? '仅手动' : '$_einkSec 秒', null), + ('已收帧数', '${_series.received}', null), + ('本会话断点', '${_series.gapCount}', null), + ('最后刷新', _einkAt, null), + ('常驻内存', '${(ProcessInfo.currentRss / 1024 / 1024).round()} MB', null), ], ); } @@ -917,7 +929,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: style.soft, - borderRadius: BorderRadius.circular(PmonRadius.pill), + borderRadius: BorderRadius.circular(InkwatchRadius.pill), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -930,7 +942,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { shape: BoxShape.circle, ), ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text( label, style: TextStyle( @@ -972,7 +984,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { size: 16, color: on ? _p.primary : _p.textTertiary, ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text(text), ], ), @@ -1058,12 +1070,12 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // 切到查看端时先给"内容正在就位"的形状,而不是一个转圈 + 空白页: // 用户能看懂在等什么,页面也不会在数据到达时突然变样(观感更连贯)。 return ListView( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), children: const [ LoadingHint(text: '正在连接网关并读取设备列表…'), - SizedBox(height: PmonSpace.lg), - SkeletonBox(height: 120, radius: PmonRadius.md), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), + SkeletonBox(height: 120, radius: InkwatchRadius.md), + SizedBox(height: InkwatchSpace.lg), SkeletonMetricGrid(cells: 6), ], ); @@ -1075,10 +1087,10 @@ class _ViewerPageState extends State with WidgetsBindingObserver { final membersDev = _membersTarget(); return ListView( padding: EdgeInsets.fromLTRB( - PmonSpace.lg, - PmonSpace.lg, - PmonSpace.lg, - PmonSpace.xxl, + InkwatchSpace.lg, + InkwatchSpace.lg, + InkwatchSpace.lg, + InkwatchSpace.xxl, ), children: [ if (_st.lastError != null) _errorCard(_st.lastError!), @@ -1087,13 +1099,13 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // 只留一行诊断信息;"需对方授权 / 服务器时间轴 / 断点不插值"这三句承诺 // 移进设备卡的脚注 —— 它们解释的是"这份列表",贴在它旁边才成立 _connCaption(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), // 主角提前:用户来这一页是为了"看设备",不是看登录状态 _pairedDevicesCard(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), if (_st.subscribed && _series.deviceId.isNotEmpty) ...[ ..._dashboard(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), ], // 登录与配对:设备列表里已逐条标注「我的 / 他人授权」, // 故这里不再是"解释列表来源的必要前提",可以放到数据后面 @@ -1109,7 +1121,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // `/members`(还要再查一次归属人),首屏就变成十几个并发请求。 // 而"谁还能看这台设备"这个问题,用户问的对象永远是**手里正在看的那台**。 if (membersDev != null) ...[ - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), DeviceMembersCard( api: _api, deviceId: membersDev.deviceId, @@ -1118,9 +1130,9 @@ class _ViewerPageState extends State with WidgetsBindingObserver { deviceLabel: membersDev.deviceName, ), ], - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), _pairingCard(), - SizedBox(height: PmonSpace.lg), + SizedBox(height: InkwatchSpace.lg), // 设备间传文件(与采集端页同一张卡片)—— // 接收这一侧在查看端更常用:"把另一台机器上的文件拉过来"。 FileTransferCard( @@ -1214,7 +1226,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { Widget _pairedDevicesCard() { return Card( child: Padding( - padding: EdgeInsets.all(PmonSpace.lg), + padding: EdgeInsets.all(InkwatchSpace.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1225,7 +1237,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { size: 18, color: _p.textSecondary, ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text('可查看的设备', style: _tx.section), Spacer(), if (!_st.presenceKnown && _st.devicesLoaded) @@ -1233,7 +1245,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { ], ), // 操作按钮从旧深色 header 搬到这里:它们本就作用于"这份列表" - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1245,12 +1257,12 @@ class _ViewerPageState extends State with WidgetsBindingObserver { style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, padding: EdgeInsets.symmetric( - horizontal: PmonSpace.md, - vertical: PmonSpace.sm, + horizontal: InkwatchSpace.md, + vertical: InkwatchSpace.sm, ), ), ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), OutlinedButton.icon( onPressed: _st.subscribed ? _stopViewing : null, icon: Icon(Icons.stop_circle_outlined, size: 16), @@ -1258,14 +1270,14 @@ class _ViewerPageState extends State with WidgetsBindingObserver { style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, padding: EdgeInsets.symmetric( - horizontal: PmonSpace.md, - vertical: PmonSpace.sm, + horizontal: InkwatchSpace.md, + vertical: InkwatchSpace.sm, ), ), ), ], ), - SizedBox(height: PmonSpace.md), + SizedBox(height: InkwatchSpace.md), if (!_st.devicesLoaded) Padding( padding: EdgeInsets.symmetric(vertical: 6), @@ -1297,7 +1309,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { // 三句承诺贴在它们解释的对象旁边(原来是页面顶部独立一行, // 离设备列表很远,还占掉首屏一行) Padding( - padding: EdgeInsets.only(top: PmonSpace.sm), + padding: EdgeInsets.only(top: InkwatchSpace.sm), child: Text('需对方授权 · 服务器时间轴 · 断点不插值', style: _tx.caption), ), ], @@ -1393,7 +1405,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { ), ), ), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Text( label, style: TextStyle( @@ -1410,7 +1422,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { Row( children: [ _relationBadge(d), - SizedBox(width: PmonSpace.sm), + SizedBox(width: InkwatchSpace.sm), Expanded( child: Text( '${d.platform} · ${d.deviceId}' @@ -1786,13 +1798,13 @@ class _ViewerPageState extends State with WidgetsBindingObserver { if (_range != 'live') unawaited(_loadHistory()); } : null, - borderRadius: BorderRadius.circular(PmonRadius.md), + borderRadius: BorderRadius.circular(InkwatchRadius.md), child: Container( padding: EdgeInsets.symmetric( - horizontal: PmonSpace.md, - vertical: PmonSpace.md, + horizontal: InkwatchSpace.md, + vertical: InkwatchSpace.md, ), - decoration: selected ? PmonCard.selected(palette: _p) : PmonCard.standard(palette: _p), + decoration: selected ? InkwatchCard.selected(palette: _p) : InkwatchCard.standard(palette: _p), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1806,7 +1818,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { ? _p.primary : _p.textTertiary, ), - SizedBox(width: PmonSpace.xs + 2), + SizedBox(width: InkwatchSpace.xs + 2), Expanded( child: Text( spec?.label ?? key, @@ -1822,7 +1834,7 @@ class _ViewerPageState extends State with WidgetsBindingObserver { ), ], ), - SizedBox(height: PmonSpace.sm), + SizedBox(height: InkwatchSpace.sm), // 数值走令牌里的"指标数值"样式:等宽数字 + 固定字重, // 数值变化时不会左右跳动(一排卡片同时刷新时尤其明显) // 自适应缩放:数组型指标("16 核 · 峰 87%")在窄卡里放不下会被截断, @@ -2175,7 +2187,7 @@ class _PulsePlaceholder extends StatefulWidget { class _PulsePlaceholderState extends State<_PulsePlaceholder> with SingleTickerProviderStateMixin { - PmonPalette get _p => PmonPalette.of(context); + InkwatchPalette get _p => InkwatchPalette.of(context); late final AnimationController _c = AnimationController( vsync: this, @@ -2188,7 +2200,7 @@ class _PulsePlaceholderState extends State<_PulsePlaceholder> // ⚠️ 水墨屏上**绝不能** repeat:无限动画 = 永不停止的局部刷新 // (残影累积、发花、耗电,而它唯一的作用只是"告知在等")。 // 墨水屏的等待用静止图标表达就够了 —— 见 `ui/eink_mode.dart`。 - if (!PmonEinkMode.enabled) _c.repeat(reverse: true); + if (!InkwatchEinkMode.enabled) _c.repeat(reverse: true); } @override @@ -2199,16 +2211,16 @@ class _PulsePlaceholderState extends State<_PulsePlaceholder> @override Widget build(BuildContext context) { - if (PmonEinkMode.enabled) { + if (InkwatchEinkMode.enabled) { // 静止版:无动画、纯黑描边圆 —— 语义相同("在等对方"),代价为零 return Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all(color: PmonEink.rule, width: 1.5), + border: Border.all(color: InkwatchEink.rule, width: 1.5), ), - child: Icon(Icons.sensors, size: 26, color: PmonEink.ink), + child: Icon(Icons.sensors, size: 26, color: InkwatchEink.ink), ); } return AnimatedBuilder( diff --git a/client/scripts/test.sh b/client/scripts/test.sh index 2732d03..9351538 100755 --- a/client/scripts/test.sh +++ b/client/scripts/test.sh @@ -50,6 +50,7 @@ REGISTERED_TESTS=( device_merge_notice_test.dart device_model_test.dart eink_board_test.dart + event_reporter_test.dart handover_test.dart history_test.dart host_ffi_test.dart @@ -64,6 +65,7 @@ REGISTERED_TESTS=( p2p_driver_test.dart p2p_session_test.dart pairing_card_test.dart + plan_text_guard_test.dart relay_driver_test.dart resident_card_test.dart resident_service_test.dart @@ -72,6 +74,7 @@ REGISTERED_TESTS=( startup_log_test.dart startup_path_test.dart stun_test.dart + symbol_guard_test.dart theme_guard_test.dart token_parity_test.dart transfer_card_test.dart diff --git a/client/test/dark_theme_test.dart b/client/test/dark_theme_test.dart index e053620..a88651e 100644 --- a/client/test/dark_theme_test.dart +++ b/client/test/dark_theme_test.dart @@ -3,9 +3,9 @@ /// 运行:cd client && bash scripts/test.sh test/dark_theme_test.dart /// /// ── 覆盖面 ──────────────────────────────────────────────────────── -/// ① buildPmonDarkTheme() 的 brightness / colorScheme / 关键色值 / 控件语义色 +/// ① buildInkwatchDarkTheme() 的 brightness / colorScheme / 关键色值 / 控件语义色 /// ② E-Ink 主题不受深色影响(纯黑白、圆角 0、无动画、看板局部覆盖解耦) -/// ③ 浅色 buildPmonTheme() 逐字段未变(重构只是把装配参数化) +/// ③ 浅色 buildInkwatchTheme() 逐字段未变(重构只是把装配参数化) /// ④ 主题偏好持久化(AppConfig 值域 light|dark、默认 light 不预写键) library; @@ -13,61 +13,61 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pmon_client/host/app_config.dart'; -import 'package:pmon_client/host/config_migration.dart'; -import 'package:pmon_client/ui/theme.dart'; +import 'package:inkwatch_client/host/app_config.dart'; +import 'package:inkwatch_client/host/config_migration.dart'; +import 'package:inkwatch_client/ui/theme.dart'; void main() { group('① 深色 ThemeData', () { test('brightness == Brightness.dark,colorScheme 与主色一致', () { - final t = buildPmonDarkTheme(); + final t = buildInkwatchDarkTheme(); expect(t.brightness, Brightness.dark); expect(t.colorScheme.brightness, Brightness.dark); // 「与主色一致」:primary 严格落在语义令牌上(不是 fromSeed 的 tonal 近似值) - expect(t.colorScheme.primary, PmonDarkColors.primary); + expect(t.colorScheme.primary, InkwatchDarkColors.primary); expect(t.colorScheme.primary, const Color(0xFF6E9CFF)); - expect(t.colorScheme.surface, PmonDarkColors.surface); - expect(t.colorScheme.onSurface, PmonDarkColors.textPrimary); - expect(t.colorScheme.onPrimary, PmonDarkColors.textOnAccent); + expect(t.colorScheme.surface, InkwatchDarkColors.surface); + expect(t.colorScheme.onSurface, InkwatchDarkColors.textPrimary); + expect(t.colorScheme.onPrimary, InkwatchDarkColors.textOnAccent); }); test('关键色值:page / surface / textPrimary / primary / tertiary / snackbar', () { - final t = buildPmonDarkTheme(); + final t = buildInkwatchDarkTheme(); // page #0B0E14 - expect(t.scaffoldBackgroundColor, PmonDarkColors.page); + expect(t.scaffoldBackgroundColor, InkwatchDarkColors.page); expect(t.scaffoldBackgroundColor, const Color(0xFF0B0E14)); // surface #151A23(AppBar / 卡片 / 弹窗 / 输入底) expect(t.appBarTheme.backgroundColor, const Color(0xFF151A23)); expect(t.cardTheme.color, const Color(0xFF151A23)); - expect(t.popupMenuTheme.color, PmonDarkColors.surface); - expect(t.inputDecorationTheme.fillColor, PmonDarkColors.surface); + expect(t.popupMenuTheme.color, InkwatchDarkColors.surface); + expect(t.inputDecorationTheme.fillColor, InkwatchDarkColors.surface); // textPrimary #F2F4F9(AppBar 标题文字) - expect(t.appBarTheme.titleTextStyle?.color, PmonDarkColors.textPrimary); - expect(PmonDarkColors.textPrimary, const Color(0xFFF2F4F9)); + expect(t.appBarTheme.titleTextStyle?.color, InkwatchDarkColors.textPrimary); + expect(InkwatchDarkColors.textPrimary, const Color(0xFFF2F4F9)); // primary #6E9CFF expect(t.colorScheme.primary, const Color(0xFF6E9CFF)); // tertiary 仍 #8A93A3 —— 显式不同步浅色点修值 #646D7D - expect(PmonDarkColors.textTertiary, const Color(0xFF8A93A3)); - expect(PmonDarkColors.textTertiary, isNot(PmonColors.textTertiary)); + expect(InkwatchDarkColors.textTertiary, const Color(0xFF8A93A3)); + expect(InkwatchDarkColors.textTertiary, isNot(InkwatchColors.textTertiary)); // snackbar 反相 #F2F4F9:浅底必须配深字,不能白字压浅底 - expect(t.snackBarTheme.backgroundColor, PmonDarkColors.snackbarBg); + expect(t.snackBarTheme.backgroundColor, InkwatchDarkColors.snackbarBg); expect(t.snackBarTheme.backgroundColor, const Color(0xFFF2F4F9)); - expect(t.snackBarTheme.contentTextStyle?.color, PmonDarkColors.surface); + expect(t.snackBarTheme.contentTextStyle?.color, InkwatchDarkColors.surface); expect(t.snackBarTheme.contentTextStyle?.color, isNot(Colors.white)); }); test('控件语义色一并换深色(沿用同一套装配,只替换色板)', () { - final t = buildPmonDarkTheme(); - expect(t.dividerTheme.color, PmonDarkColors.line); - expect(t.popupMenuTheme.textStyle?.color, PmonDarkColors.textPrimary); + final t = buildInkwatchDarkTheme(); + expect(t.dividerTheme.color, InkwatchDarkColors.line); + expect(t.popupMenuTheme.textStyle?.color, InkwatchDarkColors.textPrimary); expect(t.inputDecorationTheme.hintStyle?.color, - PmonDarkColors.textTertiary); + InkwatchDarkColors.textTertiary); expect(t.inputDecorationTheme.focusedBorder?.borderSide.color, - PmonDarkColors.primary); - expect(t.listTileTheme.iconColor, PmonDarkColors.textTertiary); - expect(t.listTileTheme.textColor, PmonDarkColors.textPrimary); - expect(t.navigationBarTheme.backgroundColor, PmonDarkColors.surface); - expect(t.navigationBarTheme.indicatorColor, PmonDarkColors.primarySoft); + InkwatchDarkColors.primary); + expect(t.listTileTheme.iconColor, InkwatchDarkColors.textTertiary); + expect(t.listTileTheme.textColor, InkwatchDarkColors.textPrimary); + expect(t.navigationBarTheme.backgroundColor, InkwatchDarkColors.surface); + expect(t.navigationBarTheme.indicatorColor, InkwatchDarkColors.primarySoft); }); }); @@ -75,16 +75,16 @@ void main() { test('einkTheme() 仍是纯黑白 / Brightness.light / 无 splash', () { final e = einkTheme(); expect(e.colorScheme.brightness, Brightness.light); - expect(e.scaffoldBackgroundColor, PmonEink.page); + expect(e.scaffoldBackgroundColor, InkwatchEink.page); expect(e.scaffoldBackgroundColor, const Color(0xFFFFFFFF)); - expect(e.colorScheme.primary, PmonEink.ink); + expect(e.colorScheme.primary, InkwatchEink.ink); expect(e.colorScheme.primary, const Color(0xFF000000)); - expect(e.cardTheme.color, PmonEink.box); - expect(e.appBarTheme.backgroundColor, PmonEink.page); + expect(e.cardTheme.color, InkwatchEink.box); + expect(e.appBarTheme.backgroundColor, InkwatchEink.page); expect(e.splashFactory, NoSplash.splashFactory); - expect(e.snackBarTheme.backgroundColor, PmonEink.ink); + expect(e.snackBarTheme.backgroundColor, InkwatchEink.ink); expect(e.snackBarTheme.backgroundColor, - isNot(PmonDarkColors.snackbarBg)); + isNot(InkwatchDarkColors.snackbarBg)); }); test('E-Ink 看板以 einkTheme() 局部覆盖主题,不读 themeMode', () { @@ -93,35 +93,35 @@ void main() { expect(src.contains('Theme('), isTrue); expect(src.contains('data: einkTheme()'), isTrue, reason: '看板自带 einkTheme(),与 MaterialApp 的 themeMode 解耦'); - expect(src.contains('PmonDarkColors'), isFalse); - expect(src.contains('buildPmonDarkTheme'), isFalse); - expect(src.contains('PmonColors'), isFalse, - reason: '看板只消费 PmonEink.* / PmonEinkText.*,不碰彩色令牌'); - expect(src.contains('PmonEinkText'), isTrue); + expect(src.contains('InkwatchDarkColors'), isFalse); + expect(src.contains('buildInkwatchDarkTheme'), isFalse); + expect(src.contains('InkwatchColors'), isFalse, + reason: '看板只消费 InkwatchEink.* / InkwatchEinkText.*,不碰彩色令牌'); + expect(src.contains('InkwatchEinkText'), isTrue); expect(src.contains('statusGlyph'), isTrue); }); }); group('③ 浅色主题未变(深色只是新增装配)', () { - test('buildPmonTheme() 仍是 Brightness.light,且色值与重构前基线一致', () { - final t = buildPmonTheme(); + test('buildInkwatchTheme() 仍是 Brightness.light,且色值与重构前基线一致', () { + final t = buildInkwatchTheme(); expect(t.brightness, Brightness.light); expect(t.colorScheme.brightness, Brightness.light); - expect(t.scaffoldBackgroundColor, PmonColors.page); + expect(t.scaffoldBackgroundColor, InkwatchColors.page); // 重构前 colorScheme 由 fromSeed 派生,primary 不是主色本身。 // 用同一公式动态取基线,钉死「浅色分支没有被 copyWith 改动」。 final fromSeedPrimary = ColorScheme.fromSeed( - seedColor: PmonColors.primary, + seedColor: InkwatchColors.primary, brightness: Brightness.light, ).primary; expect(t.colorScheme.primary, fromSeedPrimary); - expect(t.colorScheme.primary, isNot(PmonColors.primary)); - expect(t.appBarTheme.backgroundColor, PmonColors.surface); - expect(t.cardTheme.color, PmonColors.surface); - expect(t.snackBarTheme.backgroundColor, PmonColors.textPrimary); + expect(t.colorScheme.primary, isNot(InkwatchColors.primary)); + expect(t.appBarTheme.backgroundColor, InkwatchColors.surface); + expect(t.cardTheme.color, InkwatchColors.surface); + expect(t.snackBarTheme.backgroundColor, InkwatchColors.textPrimary); expect(t.snackBarTheme.contentTextStyle?.color, Colors.white); // 深浅确实是两套不同的主题 - expect(buildPmonDarkTheme().brightness, isNot(t.brightness)); + expect(buildInkwatchDarkTheme().brightness, isNot(t.brightness)); }); }); diff --git a/client/test/dark_ui_test.dart b/client/test/dark_ui_test.dart index 66ba8ba..1894d55 100644 --- a/client/test/dark_ui_test.dart +++ b/client/test/dark_ui_test.dart @@ -3,25 +3,25 @@ /// 运行:cd client && bash scripts/test.sh test/dark_ui_test.dart /// /// ── 覆盖面 ──────────────────────────────────────────────────────── -/// ① 三份 PmonPalette 字段映射:light 逐项 == PmonColors、dark 逐项 == -/// PmonDarkColors、eink 全部 R=G=B 无彩色;statusStyle 按色板分叉。 +/// ① 三份 InkwatchPalette 字段映射:light 逐项 == InkwatchColors、dark 逐项 == +/// InkwatchDarkColors、eink 全部 R=G=B 无彩色;statusStyle 按色板分叉。 /// ② 三个 ThemeData(含 einkTheme)都挂了对应 extension —— E-Ink 不得为 null。 /// ③ 浅 / 深 / E-Ink 三主题下渲染关键自绘组件(指标卡 / 状态徽标 / /// 标准卡片 / 提示条 / 说明文字),断言实际 BoxDecoration.color / /// TextStyle.color / statusStyle 返回值分别等于三套色板。 /// ④ E-Ink 独立性:einkTheme() 与深色外壳下看板仍纯黑白、圆角 0, -/// 且 Theme(data: einkTheme()) 局部覆盖里 PmonPalette.of(ctx) 取到 eink。 +/// 且 Theme(data: einkTheme()) 局部覆盖里 InkwatchPalette.of(ctx) 取到 eink。 library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pmon_client/host/metric_format.dart'; -import 'package:pmon_client/ui/eink_board.dart'; -import 'package:pmon_client/ui/skeleton.dart'; -import 'package:pmon_client/ui/theme.dart'; +import 'package:inkwatch_client/host/metric_format.dart'; +import 'package:inkwatch_client/ui/eink_board.dart'; +import 'package:inkwatch_client/ui/skeleton.dart'; +import 'package:inkwatch_client/ui/theme.dart'; /// 把色板拆成「字段名 → 颜色」,便于逐项比对三份实例。 -Map _fields(PmonPalette p) => { +Map _fields(InkwatchPalette p) => { 'page': p.page, 'surface': p.surface, 'surfaceMuted': p.surfaceMuted, @@ -49,57 +49,57 @@ Map _fields(PmonPalette p) => { }; Map _lightRef() => { - 'page': PmonColors.page, - 'surface': PmonColors.surface, - 'surfaceMuted': PmonColors.surfaceMuted, - 'line': PmonColors.line, - 'textPrimary': PmonColors.textPrimary, - 'textSecondary': PmonColors.textSecondary, - 'textTertiary': PmonColors.textTertiary, - 'primary': PmonColors.primary, - 'primarySoft': PmonColors.primarySoft, - 'online': PmonColors.online, - 'onlineSoft': PmonColors.onlineSoft, - 'onlineText': PmonColors.onlineText, - 'suspect': PmonColors.suspect, - 'suspectSoft': PmonColors.suspectSoft, - 'suspectText': PmonColors.suspectText, - 'offline': PmonColors.offline, - 'offlineText': PmonColors.offlineText, - 'unknownText': PmonColors.unknownText, - 'danger': PmonColors.danger, - 'dangerSoft': PmonColors.dangerSoft, - 'dangerText': PmonColors.dangerText, - 'infoText': PmonColors.infoText, - 'chartLine': PmonColors.chartLine, - 'gap': PmonColors.gap, + 'page': InkwatchColors.page, + 'surface': InkwatchColors.surface, + 'surfaceMuted': InkwatchColors.surfaceMuted, + 'line': InkwatchColors.line, + 'textPrimary': InkwatchColors.textPrimary, + 'textSecondary': InkwatchColors.textSecondary, + 'textTertiary': InkwatchColors.textTertiary, + 'primary': InkwatchColors.primary, + 'primarySoft': InkwatchColors.primarySoft, + 'online': InkwatchColors.online, + 'onlineSoft': InkwatchColors.onlineSoft, + 'onlineText': InkwatchColors.onlineText, + 'suspect': InkwatchColors.suspect, + 'suspectSoft': InkwatchColors.suspectSoft, + 'suspectText': InkwatchColors.suspectText, + 'offline': InkwatchColors.offline, + 'offlineText': InkwatchColors.offlineText, + 'unknownText': InkwatchColors.unknownText, + 'danger': InkwatchColors.danger, + 'dangerSoft': InkwatchColors.dangerSoft, + 'dangerText': InkwatchColors.dangerText, + 'infoText': InkwatchColors.infoText, + 'chartLine': InkwatchColors.chartLine, + 'gap': InkwatchColors.gap, }; Map _darkRef() => { - 'page': PmonDarkColors.page, - 'surface': PmonDarkColors.surface, - 'surfaceMuted': PmonDarkColors.surfaceMuted, - 'line': PmonDarkColors.line, - 'textPrimary': PmonDarkColors.textPrimary, - 'textSecondary': PmonDarkColors.textSecondary, - 'textTertiary': PmonDarkColors.textTertiary, - 'primary': PmonDarkColors.primary, - 'primarySoft': PmonDarkColors.primarySoft, - 'online': PmonDarkColors.online, - 'onlineSoft': PmonDarkColors.onlineSoft, - 'onlineText': PmonDarkColors.onlineText, - 'suspect': PmonDarkColors.suspect, - 'suspectSoft': PmonDarkColors.suspectSoft, - 'suspectText': PmonDarkColors.suspectText, - 'offline': PmonDarkColors.offline, - 'offlineText': PmonDarkColors.offlineText, - 'unknownText': PmonDarkColors.unknownText, - 'danger': PmonDarkColors.danger, - 'dangerSoft': PmonDarkColors.dangerSoft, - 'dangerText': PmonDarkColors.dangerText, - 'infoText': PmonDarkColors.infoText, - 'chartLine': PmonDarkColors.chartLine, - 'gap': PmonDarkColors.gap, + 'page': InkwatchDarkColors.page, + 'surface': InkwatchDarkColors.surface, + 'surfaceMuted': InkwatchDarkColors.surfaceMuted, + 'line': InkwatchDarkColors.line, + 'textPrimary': InkwatchDarkColors.textPrimary, + 'textSecondary': InkwatchDarkColors.textSecondary, + 'textTertiary': InkwatchDarkColors.textTertiary, + 'primary': InkwatchDarkColors.primary, + 'primarySoft': InkwatchDarkColors.primarySoft, + 'online': InkwatchDarkColors.online, + 'onlineSoft': InkwatchDarkColors.onlineSoft, + 'onlineText': InkwatchDarkColors.onlineText, + 'suspect': InkwatchDarkColors.suspect, + 'suspectSoft': InkwatchDarkColors.suspectSoft, + 'suspectText': InkwatchDarkColors.suspectText, + 'offline': InkwatchDarkColors.offline, + 'offlineText': InkwatchDarkColors.offlineText, + 'unknownText': InkwatchDarkColors.unknownText, + 'danger': InkwatchDarkColors.danger, + 'dangerSoft': InkwatchDarkColors.dangerSoft, + 'dangerText': InkwatchDarkColors.dangerText, + 'infoText': InkwatchDarkColors.infoText, + 'chartLine': InkwatchDarkColors.chartLine, + 'gap': InkwatchDarkColors.gap, }; /// 无彩色 = R=G=B(纯黑 / 纯白 / 中性灰都算,带色相的彩色不算)。 @@ -112,19 +112,19 @@ class _PaletteProbe extends StatelessWidget { @override Widget build(BuildContext context) { - final p = PmonPalette.of(context); + final p = InkwatchPalette.of(context); final s = statusStyle('online', p); return SingleChildScrollView( child: Column( children: [ - const SkeletonMetricCard(), // 标准卡片 / 指标卡(PmonCard.standard) + const SkeletonMetricCard(), // 标准卡片 / 指标卡(InkwatchCard.standard) const SkeletonBox(width: 40, height: 10), // 次级表面色块 - const LoadingHint(text: '正在加载…'), // 说明文字(PmonTextStyles.caption) + const LoadingHint(text: '正在加载…'), // 说明文字(InkwatchTextStyles.caption) Container( key: const Key('notice'), width: 120, height: 20, - decoration: PmonCard.notice(p.suspect, p.suspectSoft), // 提示条 + decoration: InkwatchCard.notice(p.suspect, p.suspectSoft), // 提示条 ), Container( key: const Key('badge'), @@ -149,16 +149,16 @@ Future _pumpProbe(WidgetTester tester, ThemeData theme) async { } /// 断言探针里每个自绘点都取当前色板 p 的对应值。 -void _expectProbe(WidgetTester tester, PmonPalette p) { +void _expectProbe(WidgetTester tester, InkwatchPalette p) { // 标准卡片:白面 + 发丝线 final card = tester.widget(find .descendant( of: find.byType(SkeletonMetricCard), matching: find.byType(Container)) .first); final cardDeco = card.decoration! as BoxDecoration; - expect(cardDeco.color, p.surface, reason: 'PmonCard.standard().color'); + expect(cardDeco.color, p.surface, reason: 'InkwatchCard.standard().color'); expect((cardDeco.border as Border).top.color, p.line, - reason: 'PmonCard.standard().border'); + reason: 'InkwatchCard.standard().border'); // 次级表面色块 final box = tester.widget(find @@ -173,12 +173,12 @@ void _expectProbe(WidgetTester tester, PmonPalette p) { .descendant(of: find.byType(LoadingHint), matching: find.byType(Text)) .first); expect(hint.style?.color, p.textTertiary, - reason: 'PmonTextStyles.caption 的 textTertiary'); + reason: 'InkwatchTextStyles.caption 的 textTertiary'); // 提示条(notice 的软底) final notice = tester.widget(find.byKey(const Key('notice'))); expect((notice.decoration! as BoxDecoration).color, p.suspectSoft, - reason: 'PmonCard.notice 的软底'); + reason: 'InkwatchCard.notice 的软底'); // 状态徽标:软底与文字色都来自 statusStyle(key, p) final s = statusStyle('online', p); @@ -266,57 +266,57 @@ void _expectBoardMonochrome(WidgetTester tester) { void main() { group('① 三份 palette 的字段映射', () { - test('light 逐项 == PmonColors(浅色视觉一字不变,24 字段全覆盖)', () { - final got = _fields(PmonPalette.light); + test('light 逐项 == InkwatchColors(浅色视觉一字不变,24 字段全覆盖)', () { + final got = _fields(InkwatchPalette.light); final ref = _lightRef(); expect(got.keys.toSet(), ref.keys.toSet()); for (final k in ref.keys) { expect(got[k], ref[k], - reason: 'PmonPalette.light.$k 必须等于 PmonColors.$k'); + reason: 'InkwatchPalette.light.$k 必须等于 InkwatchColors.$k'); } }); - test('dark 逐项 == PmonDarkColors(深色整屏生效的根)', () { - final got = _fields(PmonPalette.dark); + test('dark 逐项 == InkwatchDarkColors(深色整屏生效的根)', () { + final got = _fields(InkwatchPalette.dark); final ref = _darkRef(); expect(got.keys.toSet(), ref.keys.toSet()); for (final k in ref.keys) { expect(got[k], ref[k], - reason: 'PmonPalette.dark.$k 必须等于 PmonDarkColors.$k'); + reason: 'InkwatchPalette.dark.$k 必须等于 InkwatchDarkColors.$k'); } }); test('深色关键色值:page/surface/textPrimary/primary/tertiary', () { - expect(PmonPalette.dark.page, const Color(0xFF0B0E14)); - expect(PmonPalette.dark.surface, const Color(0xFF151A23)); - expect(PmonPalette.dark.textPrimary, const Color(0xFFF2F4F9)); - expect(PmonPalette.dark.primary, const Color(0xFF6E9CFF)); + expect(InkwatchPalette.dark.page, const Color(0xFF0B0E14)); + expect(InkwatchPalette.dark.surface, const Color(0xFF151A23)); + expect(InkwatchPalette.dark.textPrimary, const Color(0xFFF2F4F9)); + expect(InkwatchPalette.dark.primary, const Color(0xFF6E9CFF)); // tertiary 显式不同步浅色点修值 #646D7D - expect(PmonPalette.dark.textTertiary, const Color(0xFF8A93A3)); - expect(PmonPalette.dark.textTertiary, - isNot(PmonPalette.light.textTertiary)); + expect(InkwatchPalette.dark.textTertiary, const Color(0xFF8A93A3)); + expect(InkwatchPalette.dark.textTertiary, + isNot(InkwatchPalette.light.textTertiary)); // 深浅确实是两套不同的色板 - for (final k in _fields(PmonPalette.light).keys) { + for (final k in _fields(InkwatchPalette.light).keys) { if (k == 'offline') continue; // offline 深浅同值(中性灰),语义如此 - expect(_fields(PmonPalette.dark)[k], - isNot(_fields(PmonPalette.light)[k]), + expect(_fields(InkwatchPalette.dark)[k], + isNot(_fields(InkwatchPalette.light)[k]), reason: 'dark.$k 不能仍是浅色值'); } }); - test('eink 全部字段 R=G=B(无彩色),且映射到 PmonEink 六色', () { - final got = _fields(PmonPalette.eink); + test('eink 全部字段 R=G=B(无彩色),且映射到 InkwatchEink 六色', () { + final got = _fields(InkwatchPalette.eink); expect(got.length, 24); for (final e in got.entries) { expect(_isGray(e.value), isTrue, - reason: 'PmonPalette.eink.${e.key} 出现了彩色'); + reason: 'InkwatchPalette.eink.${e.key} 出现了彩色'); } - expect(PmonPalette.eink.page, PmonEink.page); - expect(PmonPalette.eink.surface, PmonEink.box); - expect(PmonPalette.eink.line, PmonEink.rule); - expect(PmonPalette.eink.textPrimary, PmonEink.ink); - expect(PmonPalette.eink.primary, PmonEink.ink); - expect(PmonPalette.eink.chartLine, PmonEink.ink); + expect(InkwatchPalette.eink.page, InkwatchEink.page); + expect(InkwatchPalette.eink.surface, InkwatchEink.box); + expect(InkwatchPalette.eink.line, InkwatchEink.rule); + expect(InkwatchPalette.eink.textPrimary, InkwatchEink.ink); + expect(InkwatchPalette.eink.primary, InkwatchEink.ink); + expect(InkwatchPalette.eink.chartLine, InkwatchEink.ink); }); test('statusStyle 按色板分叉(6 键),单参数旧签名仍回退浅色', () { @@ -328,76 +328,76 @@ void main() { 'info', 'anything-else' ]) { - final l = statusStyle(key, PmonPalette.light); - final d = statusStyle(key, PmonPalette.dark); - final e = statusStyle(key, PmonPalette.eink); + final l = statusStyle(key, InkwatchPalette.light); + final d = statusStyle(key, InkwatchPalette.dark); + final e = statusStyle(key, InkwatchPalette.eink); // label 与色板无关 expect(l.label, d.label); expect(l.label, e.label); // 颜色严格来自各自色板(钉死「不再写死浅色常量」) - expect(l.color, statusStyle(key, PmonPalette.light).color); + expect(l.color, statusStyle(key, InkwatchPalette.light).color); expect(_isGray(e.color), isTrue, reason: '$key eink.color 有彩色'); expect(_isGray(e.soft), isTrue, reason: '$key eink.soft 有彩色'); expect(_isGray(e.text), isTrue, reason: '$key eink.text 有彩色'); } - expect(statusStyle('online', PmonPalette.light).color, - PmonColors.online); - expect(statusStyle('online', PmonPalette.dark).color, - PmonDarkColors.online); - expect(statusStyle('online', PmonPalette.dark).color, - isNot(statusStyle('online', PmonPalette.light).color)); + expect(statusStyle('online', InkwatchPalette.light).color, + InkwatchColors.online); + expect(statusStyle('online', InkwatchPalette.dark).color, + InkwatchDarkColors.online); + expect(statusStyle('online', InkwatchPalette.dark).color, + isNot(statusStyle('online', InkwatchPalette.light).color)); // 单参数(theme_guard_test 仍在用)必须仍是浅色 - expect(statusStyle('online').color, PmonColors.online); - expect(statusStyle('online').soft, PmonColors.onlineSoft); - expect(statusStyle('online').text, PmonColors.onlineText); + expect(statusStyle('online').color, InkwatchColors.online); + expect(statusStyle('online').soft, InkwatchColors.onlineSoft); + expect(statusStyle('online').text, InkwatchColors.onlineText); }); }); group('② 三个 ThemeData 都挂了对应 palette', () { test('light / dark / eink 各挂一份,E-Ink 绝不为 null', () { - expect(buildPmonTheme().extension(), - same(PmonPalette.light)); - expect(buildPmonDarkTheme().extension(), - same(PmonPalette.dark)); - expect(einkTheme().extension(), same(PmonPalette.eink)); + expect(buildInkwatchTheme().extension(), + same(InkwatchPalette.light)); + expect(buildInkwatchDarkTheme().extension(), + same(InkwatchPalette.dark)); + expect(einkTheme().extension(), same(InkwatchPalette.eink)); }); - test('PmonCard.standard()/selected() 缺省回退浅色、显式传板分叉', () { - final std = PmonCard.standard(); - expect(std.color, PmonColors.surface); - expect((std.border as Border).top.color, PmonColors.line); - final sel = PmonCard.selected(); - expect(sel.color, PmonColors.primarySoft); - expect((sel.border as Border).top.color, PmonColors.primary); + test('InkwatchCard.standard()/selected() 缺省回退浅色、显式传板分叉', () { + final std = InkwatchCard.standard(); + expect(std.color, InkwatchColors.surface); + expect((std.border as Border).top.color, InkwatchColors.line); + final sel = InkwatchCard.selected(); + expect(sel.color, InkwatchColors.primarySoft); + expect((sel.border as Border).top.color, InkwatchColors.primary); - final dStd = PmonCard.standard(palette: PmonPalette.dark); - expect(dStd.color, PmonDarkColors.surface); - expect((dStd.border as Border).top.color, PmonDarkColors.line); - final dSel = PmonCard.selected(palette: PmonPalette.dark); - expect(dSel.color, PmonDarkColors.primarySoft); - expect((dSel.border as Border).top.color, PmonDarkColors.primary); + final dStd = InkwatchCard.standard(palette: InkwatchPalette.dark); + expect(dStd.color, InkwatchDarkColors.surface); + expect((dStd.border as Border).top.color, InkwatchDarkColors.line); + final dSel = InkwatchCard.selected(palette: InkwatchPalette.dark); + expect(dSel.color, InkwatchDarkColors.primarySoft); + expect((dSel.border as Border).top.color, InkwatchDarkColors.primary); - final eStd = PmonCard.standard(palette: PmonPalette.eink); + final eStd = InkwatchCard.standard(palette: InkwatchPalette.eink); expect(_isGray(eStd.color!), isTrue); - expect((eStd.border as Border).top.color, PmonEink.rule); + expect((eStd.border as Border).top.color, InkwatchEink.rule); }); }); group('③ 三主题下渲染关键自绘组件', () { - testWidgets('浅色:自绘点全部 == PmonPalette.light', (tester) async { - await _pumpProbe(tester, buildPmonTheme()); - _expectProbe(tester, PmonPalette.light); + testWidgets('浅色:自绘点全部 == InkwatchPalette.light', (tester) async { + await _pumpProbe(tester, buildInkwatchTheme()); + _expectProbe(tester, InkwatchPalette.light); }); - testWidgets('深色:同一批自绘点全部 == PmonPalette.dark', (tester) async { - await _pumpProbe(tester, buildPmonDarkTheme()); - _expectProbe(tester, PmonPalette.dark); + testWidgets('深色:同一批自绘点全部 == InkwatchPalette.dark', (tester) async { + await _pumpProbe(tester, buildInkwatchDarkTheme()); + _expectProbe(tester, InkwatchPalette.dark); }); - testWidgets('E-Ink:同一批自绘点全部 == PmonPalette.eink 且无彩色', + testWidgets('E-Ink:同一批自绘点全部 == InkwatchPalette.eink 且无彩色', (tester) async { await _pumpProbe(tester, einkTheme()); - _expectProbe(tester, PmonPalette.eink); + _expectProbe(tester, InkwatchPalette.eink); // E-Ink 下探针里所有 BoxDecoration.color 都无彩色 for (final c in tester.widgetList(find.byType(Container))) { final d = c.decoration; @@ -421,29 +421,29 @@ void main() { testWidgets('深色 MaterialApp 外壳下看板仍无彩色(内部 einkTheme 覆盖生效)', (tester) async { await _pumpBoard(tester, - outer: buildPmonDarkTheme(), mode: ThemeMode.dark); + outer: buildInkwatchDarkTheme(), mode: ThemeMode.dark); _expectBoardMonochrome(tester); }); - testWidgets('Theme(data: einkTheme()) 局部覆盖里 PmonPalette.of(ctx) == eink', + testWidgets('Theme(data: einkTheme()) 局部覆盖里 InkwatchPalette.of(ctx) == eink', (tester) async { // 直接模拟看板 eink_board.dart:296 的局部覆盖,且外层故意是深色, // 钉死「页面改成 extension 查询后,看板里取到 null 会崩」这个最大风险。 - PmonPalette? seen; + InkwatchPalette? seen; await tester.pumpWidget(MaterialApp( - theme: buildPmonDarkTheme(), - darkTheme: buildPmonDarkTheme(), + theme: buildInkwatchDarkTheme(), + darkTheme: buildInkwatchDarkTheme(), themeMode: ThemeMode.dark, home: Theme( data: einkTheme(), child: Builder(builder: (ctx) { - seen = PmonPalette.of(ctx); + seen = InkwatchPalette.of(ctx); return const SizedBox.shrink(); }), ), )); - expect(seen, same(PmonPalette.eink)); - expect(seen, isNot(same(PmonPalette.dark))); + expect(seen, same(InkwatchPalette.eink)); + expect(seen, isNot(same(InkwatchPalette.dark))); }); }); } diff --git a/client/test/theme_guard_test.dart b/client/test/theme_guard_test.dart index 5134622..6e72416 100644 --- a/client/test/theme_guard_test.dart +++ b/client/test/theme_guard_test.dart @@ -22,7 +22,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pmon_client/ui/theme.dart'; +import 'package:inkwatch_client/ui/theme.dart'; /// 需要守住的界面文件(新增页面/组件请加到这里,否则它不受护栏保护) const List _uiFiles = [ @@ -46,7 +46,7 @@ const List _uiFiles = [ 'lib/ui/device_members_card.dart', ]; -/// 本轮按 `PmonText` 档位收敛过的界面文件(05 §3.4)。 +/// 本轮按 `InkwatchText` 档位收敛过的界面文件(05 §3.4)。 /// 这些文件里**一个** `fontSize: 数字` 都不许有 —— 与 E-Ink 分支 /// 「界面里不得写就地字号」同一纪律。新增已收敛文件务必登记到这里。 const List _fontSizeCleanFiles = [ @@ -79,7 +79,7 @@ void main() { offenders, isEmpty, reason: - '这些地方又写死了色值,请改用 PmonColors / PmonCard:\n${offenders.join('\n')}', + '这些地方又写死了色值,请改用 InkwatchColors / InkwatchCard:\n${offenders.join('\n')}', ); }); @@ -99,15 +99,15 @@ void main() { offenders, isEmpty, reason: - '中性色请用 PmonColors.textPrimary/textSecondary/textTertiary/line:\n' + '中性色请用 InkwatchColors.textPrimary/textSecondary/textTertiary/line:\n' '${offenders.join('\n')}', ); }); - test('主题装配只有一处:MaterialApp 用 buildPmonTheme()', () { + test('主题装配只有一处:MaterialApp 用 buildInkwatchTheme()', () { final main = File('lib/main.dart').readAsStringSync(); expect( - main.contains('buildPmonTheme()'), + main.contains('buildInkwatchTheme()'), isTrue, reason: 'MaterialApp 必须装配 ui/theme.dart 的主题,' @@ -117,7 +117,7 @@ void main() { expect( RegExp(r'theme:\s*ThemeData\(').hasMatch(main), isFalse, - reason: '不要在页面里就地 new ThemeData —— 主题只在 buildPmonTheme() 里装配', + reason: '不要在页面里就地 new ThemeData —— 主题只在 buildInkwatchTheme() 里装配', ); }); @@ -138,7 +138,7 @@ void main() { expect( offenders, isEmpty, - reason: '10 / 10.5 / 11.5 已收敛到 PmonText.micro / caption,' + reason: '10 / 10.5 / 11.5 已收敛到 InkwatchText.micro / caption,' '要新尺寸请在 theme.dart 加语义档位:\n${offenders.join('\n')}', ); }); @@ -153,42 +153,42 @@ void main() { total, lessThanOrEqualTo(_fontSizeBaseline), reason: '现有 $_fontSizeBaseline 处是历史存量;新增字号一律走 ' - 'PmonText 语义档位,不许再写就地数字(当前 $total 处)', + 'InkwatchText 语义档位,不许再写就地数字(当前 $total 处)', ); }); }); group('② 令牌本身可用且自洽', () { test('状态语义色映射齐全(6 行:online/suspect/offline/unknown/info/default,含文字色)', () { - expect(statusStyle('online').color, PmonColors.online); - expect(statusStyle('online').soft, PmonColors.onlineSoft); - expect(statusStyle('online').text, PmonColors.onlineText); + expect(statusStyle('online').color, InkwatchColors.online); + expect(statusStyle('online').soft, InkwatchColors.onlineSoft); + expect(statusStyle('online').text, InkwatchColors.onlineText); - expect(statusStyle('suspect').color, PmonColors.suspect); - expect(statusStyle('suspect').soft, PmonColors.suspectSoft); - expect(statusStyle('suspect').text, PmonColors.suspectText); + expect(statusStyle('suspect').color, InkwatchColors.suspect); + expect(statusStyle('suspect').soft, InkwatchColors.suspectSoft); + expect(statusStyle('suspect').text, InkwatchColors.suspectText); - expect(statusStyle('offline').color, PmonColors.offline); - expect(statusStyle('offline').soft, PmonColors.surfaceMuted); - expect(statusStyle('offline').text, PmonColors.offlineText); + expect(statusStyle('offline').color, InkwatchColors.offline); + expect(statusStyle('offline').soft, InkwatchColors.surfaceMuted); + expect(statusStyle('offline').text, InkwatchColors.offlineText); // 未知**必须**是中性灰:把"不知道"染成告警色等于编造结论(诚实边界) - expect(statusStyle('unknown').color, PmonColors.offline); + expect(statusStyle('unknown').color, InkwatchColors.offline); expect(statusStyle('unknown').label, '状态未知'); - expect(statusStyle('unknown').soft, PmonColors.surfaceMuted); - expect(statusStyle('unknown').text, PmonColors.unknownText); + expect(statusStyle('unknown').soft, InkwatchColors.surfaceMuted); + expect(statusStyle('unknown').text, InkwatchColors.unknownText); // info:提示态走主色系,文字色独立(primarySoft 底 6.73:1) expect(statusStyle('info').label, '提示'); - expect(statusStyle('info').color, PmonColors.primary); - expect(statusStyle('info').soft, PmonColors.primarySoft); - expect(statusStyle('info').text, PmonColors.infoText); + expect(statusStyle('info').color, InkwatchColors.primary); + expect(statusStyle('info').soft, InkwatchColors.primarySoft); + expect(statusStyle('info').text, InkwatchColors.infoText); // default 与 unknown 同值,不能漏掉这条兜底分支 expect(statusStyle('anything-else').label, '状态未知'); - expect(statusStyle('anything-else').color, PmonColors.offline); - expect(statusStyle('anything-else').soft, PmonColors.surfaceMuted); - expect(statusStyle('anything-else').text, PmonColors.unknownText); + expect(statusStyle('anything-else').color, InkwatchColors.offline); + expect(statusStyle('anything-else').soft, InkwatchColors.surfaceMuted); + expect(statusStyle('anything-else').text, InkwatchColors.unknownText); }); test('offlineText / unknownText 语义独立命名(不借道 textSecondary)', () { @@ -211,15 +211,15 @@ void main() { test("C-15':允许正式深色皮肤装配(不跟随系统)", () { final src = File('lib/ui/theme.dart').readAsStringSync(); for (final required in [ - 'PmonDarkColors', - 'buildPmonDarkTheme', + 'InkwatchDarkColors', + 'buildInkwatchDarkTheme', 'Brightness.dark', ]) { expect(src.contains(required), isTrue, reason: '深色皮肤是本阶段正式交付(替代旧的「本轮不引入」断言)'); } final main = File('lib/main.dart').readAsStringSync(); - expect(main.contains('darkTheme: buildPmonDarkTheme()'), isTrue, + expect(main.contains('darkTheme: buildInkwatchDarkTheme()'), isTrue, reason: 'MaterialApp 必须装配深色主题'); expect(main.contains('themeMode:'), isTrue, reason: 'themeMode 必须由应用内显式偏好驱动'); @@ -256,18 +256,18 @@ void main() { expect(darkKeys.length, 58, reason: '05 §1.4 的 58 个同名键覆写'); final src = File('lib/ui/theme.dart').readAsStringSync(); - final body = RegExp(r'class PmonDarkColors\s*\{([\s\S]*?)\n\}') + final body = RegExp(r'class InkwatchDarkColors\s*\{([\s\S]*?)\n\}') .firstMatch(src) ?.group(1) ?? ''; - expect(body.length, greaterThan(200), reason: 'PmonDarkColors 读不到内容?'); + expect(body.length, greaterThan(200), reason: 'InkwatchDarkColors 读不到内容?'); final mentioned = RegExp(r'--iw-[a-z0-9-]+') .allMatches(body) .map((m) => m.group(0)!) .toSet(); final stray = mentioned.where((k) => !darkKeys.contains(k)).toList(); expect(stray, isEmpty, - reason: 'PmonDarkColors 引用了 dark 块之外的键名(会漏进 E-Ink 分支):\n' + reason: 'InkwatchDarkColors 引用了 dark 块之外的键名(会漏进 E-Ink 分支):\n' '${stray.join('\n')}'); }); @@ -278,30 +278,30 @@ void main() { // einkTheme() 是文件最后一个顶层声明,取到末尾即可覆盖整段函数体。 final body = src.substring(start); for (final forbidden in [ - 'PmonDarkColors', + 'InkwatchDarkColors', 'Brightness.dark', - 'buildPmonDarkTheme', + 'buildInkwatchDarkTheme', ]) { expect(body.contains(forbidden), isFalse, reason: 'E-Ink 是 256 灰阶物理设备,深色皮肤一个键都不许进来'); } - expect(body.contains('PmonEink.ink'), isTrue); + expect(body.contains('InkwatchEink.ink'), isTrue); expect(body.contains('NoSplash.splashFactory'), isTrue); expect(body.contains('Brightness.light'), isTrue); }); - test("C-15':不新增圆角/间距/字号刻度 —— PmonDarkColors 只有颜色常量", () { + test("C-15':不新增圆角/间距/字号刻度 —— InkwatchDarkColors 只有颜色常量", () { final src = File('lib/ui/theme.dart').readAsStringSync(); - final body = RegExp(r'class PmonDarkColors\s*\{([\s\S]*?)\n\}') + final body = RegExp(r'class InkwatchDarkColors\s*\{([\s\S]*?)\n\}') .firstMatch(src) ?.group(1) ?? ''; final clean = body.replaceAll(RegExp(r'//[^\n]*'), ''); expect(RegExp(r'TextStyle\(').hasMatch(clean), isFalse, - reason: '深色不得新增字号档位(沿用 PmonText)'); + reason: '深色不得新增字号档位(沿用 InkwatchText)'); expect(RegExp(r'fontSize:').hasMatch(clean), isFalse); expect(RegExp(r'static const \w+\s*=\s*\d').hasMatch(clean), isFalse, - reason: '深色不得新增间距/圆角数字刻度(沿用 PmonSpace / PmonRadius)'); + reason: '深色不得新增间距/圆角数字刻度(沿用 InkwatchSpace / InkwatchRadius)'); final defs = RegExp(r'static const (\w+)\s*=\s*([^;]+);') .allMatches(clean) .toList(); @@ -312,46 +312,46 @@ void main() { RegExp(r'^Color\(0xFF[0-9A-Fa-f]{6}\)$').hasMatch(rhs) || RegExp(r'^\w+$').hasMatch(rhs), // 语义别名(chartLine = primary) isTrue, - reason: 'PmonDarkColors.${m.group(1)} 不是颜色常量(=$rhs)', + reason: 'InkwatchDarkColors.${m.group(1)} 不是颜色常量(=$rhs)', ); } }); test("C-15':浅色 18 个品牌原色未被改动(深色只是同名键覆写)", () { - // 18 原色 = PmonColors 里除 6 个 *-text 外的全部静态色值常量。 - expect(PmonColors.page, const Color(0xFFF6F7F9)); - expect(PmonColors.surface, const Color(0xFFFFFFFF)); - expect(PmonColors.line, const Color(0xFFE5E7EB)); - expect(PmonColors.surfaceMuted, const Color(0xFFF3F4F6)); - expect(PmonColors.textPrimary, const Color(0xFF111827)); - expect(PmonColors.textSecondary, const Color(0xFF4B5563)); - expect(PmonColors.textTertiary, const Color(0xFF646D7D)); - expect(PmonColors.primary, const Color(0xFF2563EB)); - expect(PmonColors.primarySoft, const Color(0xFFEFF4FF)); - expect(PmonColors.online, const Color(0xFF16A34A)); - expect(PmonColors.onlineSoft, const Color(0xFFEAF7EF)); - expect(PmonColors.suspect, const Color(0xFFD97706)); - expect(PmonColors.suspectSoft, const Color(0xFFFEF6E7)); - expect(PmonColors.offline, const Color(0xFF9CA3AF)); - expect(PmonColors.danger, const Color(0xFFDC2626)); - expect(PmonColors.dangerSoft, const Color(0xFFFDECEC)); - expect(PmonColors.chartLine, PmonColors.primary); - expect(PmonColors.gap, const Color(0xFFB6BCC8)); + // 18 原色 = InkwatchColors 里除 6 个 *-text 外的全部静态色值常量。 + expect(InkwatchColors.page, const Color(0xFFF6F7F9)); + expect(InkwatchColors.surface, const Color(0xFFFFFFFF)); + expect(InkwatchColors.line, const Color(0xFFE5E7EB)); + expect(InkwatchColors.surfaceMuted, const Color(0xFFF3F4F6)); + expect(InkwatchColors.textPrimary, const Color(0xFF111827)); + expect(InkwatchColors.textSecondary, const Color(0xFF4B5563)); + expect(InkwatchColors.textTertiary, const Color(0xFF646D7D)); + expect(InkwatchColors.primary, const Color(0xFF2563EB)); + expect(InkwatchColors.primarySoft, const Color(0xFFEFF4FF)); + expect(InkwatchColors.online, const Color(0xFF16A34A)); + expect(InkwatchColors.onlineSoft, const Color(0xFFEAF7EF)); + expect(InkwatchColors.suspect, const Color(0xFFD97706)); + expect(InkwatchColors.suspectSoft, const Color(0xFFFEF6E7)); + expect(InkwatchColors.offline, const Color(0xFF9CA3AF)); + expect(InkwatchColors.danger, const Color(0xFFDC2626)); + expect(InkwatchColors.dangerSoft, const Color(0xFFFDECEC)); + expect(InkwatchColors.chartLine, InkwatchColors.primary); + expect(InkwatchColors.gap, const Color(0xFFB6BCC8)); }); test('卡片装饰来自同一处(标准卡不用阴影,靠发丝线分层)', () { - final d = PmonCard.standard(); - expect((d.border as Border).top.color, PmonColors.line); + final d = InkwatchCard.standard(); + expect((d.border as Border).top.color, InkwatchColors.line); expect(d.boxShadow, isNull, reason: '极简取向:卡片靠留白与发丝线分层,不用阴影'); - final s = PmonCard.selected(); - expect((s.border as Border).top.color, PmonColors.primary); + final s = InkwatchCard.selected(); + expect((s.border as Border).top.color, InkwatchColors.primary); }); test('文字样式三级 + 数值样式齐全,且数值用等宽数字', () { - expect(PmonText.title.color, PmonColors.textPrimary); - expect(PmonText.caption.color, PmonColors.textTertiary); + expect(InkwatchText.title.color, InkwatchColors.textPrimary); + expect(InkwatchText.caption.color, InkwatchColors.textTertiary); expect( - PmonText.metric.fontFeatures, + InkwatchText.metric.fontFeatures, isNotNull, reason: '指标数值必须等宽,否则一排卡片刷新时数字会左右跳动', ); diff --git a/client/test/token_parity_test.dart b/client/test/token_parity_test.dart index 47eb70e..a4713f0 100644 --- a/client/test/token_parity_test.dart +++ b/client/test/token_parity_test.dart @@ -16,7 +16,7 @@ /// 3. 文件读不到内容直接 fail(`expect(src.length, greaterThan(200))`)—— /// 照抄 `theme_guard_test.dart:56` 的自检,避免路径写错静默通过。 /// 4. **深色已落地**(本阶段):`html[data-iw-theme="dark"]` 的 58 个同名键 -/// 另起一组 parity —— 转录项逐项比 `PmonDarkColors`、未转录的 Web 专属键 +/// 另起一组 parity —— 转录项逐项比 `InkwatchDarkColors`、未转录的 Web 专属键 /// 登记 `kDarkExemptTokens` 豁免,两向缺项都判红(见 group ④)。 library; @@ -35,101 +35,101 @@ typedef ParityEntry = ({String cssKey, String dartExpr, String kind}); /// · `dartOnly`—— 只 Dart 有;`cssKey` 是「不该存在的键名」,必须缺席 const List kTokenParity = [ // ── 颜色:两侧都有常量(01 §8.3「继」+ 05 §3.1 新增 6 个 *-text)── - (cssKey: '--iw-page', dartExpr: 'PmonColors.page', kind: 'both'), - (cssKey: '--iw-surface', dartExpr: 'PmonColors.surface', kind: 'both'), - (cssKey: '--iw-surface-muted', dartExpr: 'PmonColors.surfaceMuted', kind: 'both'), - (cssKey: '--iw-line', dartExpr: 'PmonColors.line', kind: 'both'), - (cssKey: '--iw-text-primary', dartExpr: 'PmonColors.textPrimary', kind: 'both'), - (cssKey: '--iw-text-secondary', dartExpr: 'PmonColors.textSecondary', kind: 'both'), - (cssKey: '--iw-text-tertiary', dartExpr: 'PmonColors.textTertiary', kind: 'both'), - (cssKey: '--iw-primary', dartExpr: 'PmonColors.primary', kind: 'both'), - (cssKey: '--iw-primary-soft', dartExpr: 'PmonColors.primarySoft', kind: 'both'), - (cssKey: '--iw-online', dartExpr: 'PmonColors.online', kind: 'both'), - (cssKey: '--iw-online-soft', dartExpr: 'PmonColors.onlineSoft', kind: 'both'), - (cssKey: '--iw-online-text', dartExpr: 'PmonColors.onlineText', kind: 'both'), - (cssKey: '--iw-suspect', dartExpr: 'PmonColors.suspect', kind: 'both'), - (cssKey: '--iw-suspect-soft', dartExpr: 'PmonColors.suspectSoft', kind: 'both'), - (cssKey: '--iw-suspect-text', dartExpr: 'PmonColors.suspectText', kind: 'both'), - (cssKey: '--iw-offline', dartExpr: 'PmonColors.offline', kind: 'both'), + (cssKey: '--iw-page', dartExpr: 'InkwatchColors.page', kind: 'both'), + (cssKey: '--iw-surface', dartExpr: 'InkwatchColors.surface', kind: 'both'), + (cssKey: '--iw-surface-muted', dartExpr: 'InkwatchColors.surfaceMuted', kind: 'both'), + (cssKey: '--iw-line', dartExpr: 'InkwatchColors.line', kind: 'both'), + (cssKey: '--iw-text-primary', dartExpr: 'InkwatchColors.textPrimary', kind: 'both'), + (cssKey: '--iw-text-secondary', dartExpr: 'InkwatchColors.textSecondary', kind: 'both'), + (cssKey: '--iw-text-tertiary', dartExpr: 'InkwatchColors.textTertiary', kind: 'both'), + (cssKey: '--iw-primary', dartExpr: 'InkwatchColors.primary', kind: 'both'), + (cssKey: '--iw-primary-soft', dartExpr: 'InkwatchColors.primarySoft', kind: 'both'), + (cssKey: '--iw-online', dartExpr: 'InkwatchColors.online', kind: 'both'), + (cssKey: '--iw-online-soft', dartExpr: 'InkwatchColors.onlineSoft', kind: 'both'), + (cssKey: '--iw-online-text', dartExpr: 'InkwatchColors.onlineText', kind: 'both'), + (cssKey: '--iw-suspect', dartExpr: 'InkwatchColors.suspect', kind: 'both'), + (cssKey: '--iw-suspect-soft', dartExpr: 'InkwatchColors.suspectSoft', kind: 'both'), + (cssKey: '--iw-suspect-text', dartExpr: 'InkwatchColors.suspectText', kind: 'both'), + (cssKey: '--iw-offline', dartExpr: 'InkwatchColors.offline', kind: 'both'), // statusStyle('offline'/'unknown').soft 就是 surfaceMuted(01 §8.3 明列) - (cssKey: '--iw-offline-soft', dartExpr: 'PmonColors.surfaceMuted', kind: 'both'), - (cssKey: '--iw-offline-text', dartExpr: 'PmonColors.offlineText', kind: 'both'), + (cssKey: '--iw-offline-soft', dartExpr: 'InkwatchColors.surfaceMuted', kind: 'both'), + (cssKey: '--iw-offline-text', dartExpr: 'InkwatchColors.offlineText', kind: 'both'), // --iw-unknown 是 offline 的同值别名(01 §8.3 括号里的说明) - (cssKey: '--iw-unknown', dartExpr: 'PmonColors.offline', kind: 'both'), - (cssKey: '--iw-unknown-soft', dartExpr: 'PmonColors.surfaceMuted', kind: 'both'), - (cssKey: '--iw-unknown-text', dartExpr: 'PmonColors.unknownText', kind: 'both'), - (cssKey: '--iw-danger', dartExpr: 'PmonColors.danger', kind: 'both'), - (cssKey: '--iw-danger-soft', dartExpr: 'PmonColors.dangerSoft', kind: 'both'), - (cssKey: '--iw-danger-text', dartExpr: 'PmonColors.dangerText', kind: 'both'), - (cssKey: '--iw-info-text', dartExpr: 'PmonColors.infoText', kind: 'both'), + (cssKey: '--iw-unknown', dartExpr: 'InkwatchColors.offline', kind: 'both'), + (cssKey: '--iw-unknown-soft', dartExpr: 'InkwatchColors.surfaceMuted', kind: 'both'), + (cssKey: '--iw-unknown-text', dartExpr: 'InkwatchColors.unknownText', kind: 'both'), + (cssKey: '--iw-danger', dartExpr: 'InkwatchColors.danger', kind: 'both'), + (cssKey: '--iw-danger-soft', dartExpr: 'InkwatchColors.dangerSoft', kind: 'both'), + (cssKey: '--iw-danger-text', dartExpr: 'InkwatchColors.dangerText', kind: 'both'), + (cssKey: '--iw-info-text', dartExpr: 'InkwatchColors.infoText', kind: 'both'), // chartLine = primary(Dart 语义别名,解析器会解引用) - (cssKey: '--iw-chart-line', dartExpr: 'PmonColors.chartLine', kind: 'both'), - (cssKey: '--iw-series-1', dartExpr: 'PmonColors.chartLine', kind: 'both'), - (cssKey: '--iw-gap', dartExpr: 'PmonColors.gap', kind: 'both'), - // E-Ink 六色:01 §8.3「PmonEink.* ↔ --iw-eink-*」 - (cssKey: '--iw-eink-page', dartExpr: 'PmonEink.page', kind: 'both'), - (cssKey: '--iw-eink-ink', dartExpr: 'PmonEink.ink', kind: 'both'), - (cssKey: '--iw-eink-ink-soft', dartExpr: 'PmonEink.inkSoft', kind: 'both'), - (cssKey: '--iw-eink-rule', dartExpr: 'PmonEink.rule', kind: 'both'), - (cssKey: '--iw-eink-box', dartExpr: 'PmonEink.box', kind: 'both'), - (cssKey: '--iw-eink-fill', dartExpr: 'PmonEink.fill', kind: 'both'), + (cssKey: '--iw-chart-line', dartExpr: 'InkwatchColors.chartLine', kind: 'both'), + (cssKey: '--iw-series-1', dartExpr: 'InkwatchColors.chartLine', kind: 'both'), + (cssKey: '--iw-gap', dartExpr: 'InkwatchColors.gap', kind: 'both'), + // E-Ink 六色:01 §8.3「InkwatchEink.* ↔ --iw-eink-*」 + (cssKey: '--iw-eink-page', dartExpr: 'InkwatchEink.page', kind: 'both'), + (cssKey: '--iw-eink-ink', dartExpr: 'InkwatchEink.ink', kind: 'both'), + (cssKey: '--iw-eink-ink-soft', dartExpr: 'InkwatchEink.inkSoft', kind: 'both'), + (cssKey: '--iw-eink-rule', dartExpr: 'InkwatchEink.rule', kind: 'both'), + (cssKey: '--iw-eink-box', dartExpr: 'InkwatchEink.box', kind: 'both'), + (cssKey: '--iw-eink-fill', dartExpr: 'InkwatchEink.fill', kind: 'both'), // ── 间距 / 圆角:两侧刻度相等(01 §8.3「继」)── - (cssKey: '--iw-space-xs', dartExpr: 'PmonSpace.xs', kind: 'both'), - (cssKey: '--iw-space-sm', dartExpr: 'PmonSpace.sm', kind: 'both'), - (cssKey: '--iw-space-md', dartExpr: 'PmonSpace.md', kind: 'both'), - (cssKey: '--iw-space-lg', dartExpr: 'PmonSpace.lg', kind: 'both'), - (cssKey: '--iw-space-xl', dartExpr: 'PmonSpace.xl', kind: 'both'), - (cssKey: '--iw-space-xxl', dartExpr: 'PmonSpace.xxl', kind: 'both'), - (cssKey: '--iw-radius-sm', dartExpr: 'PmonRadius.sm', kind: 'both'), - (cssKey: '--iw-radius-md', dartExpr: 'PmonRadius.md', kind: 'both'), - (cssKey: '--iw-radius-lg', dartExpr: 'PmonRadius.lg', kind: 'both'), - (cssKey: '--iw-radius-pill', dartExpr: 'PmonRadius.pill', kind: 'both'), + (cssKey: '--iw-space-xs', dartExpr: 'InkwatchSpace.xs', kind: 'both'), + (cssKey: '--iw-space-sm', dartExpr: 'InkwatchSpace.sm', kind: 'both'), + (cssKey: '--iw-space-md', dartExpr: 'InkwatchSpace.md', kind: 'both'), + (cssKey: '--iw-space-lg', dartExpr: 'InkwatchSpace.lg', kind: 'both'), + (cssKey: '--iw-space-xl', dartExpr: 'InkwatchSpace.xl', kind: 'both'), + (cssKey: '--iw-space-xxl', dartExpr: 'InkwatchSpace.xxl', kind: 'both'), + (cssKey: '--iw-radius-sm', dartExpr: 'InkwatchRadius.sm', kind: 'both'), + (cssKey: '--iw-radius-md', dartExpr: 'InkwatchRadius.md', kind: 'both'), + (cssKey: '--iw-radius-lg', dartExpr: 'InkwatchRadius.lg', kind: 'both'), + (cssKey: '--iw-radius-pill', dartExpr: 'InkwatchRadius.pill', kind: 'both'), // ── 字阶:CSS 是 calc(基准 × scale),按基准值比(05 §3.4)── - (cssKey: '--iw-fs-metric', dartExpr: 'PmonText.metric', kind: 'derived'), - (cssKey: '--iw-fs-title', dartExpr: 'PmonText.title', kind: 'derived'), - (cssKey: '--iw-fs-stat', dartExpr: 'PmonText.stat', kind: 'derived'), - (cssKey: '--iw-fs-section', dartExpr: 'PmonText.section', kind: 'derived'), - (cssKey: '--iw-fs-body', dartExpr: 'PmonText.body', kind: 'derived'), - (cssKey: '--iw-fs-caption', dartExpr: 'PmonText.caption', kind: 'derived'), - (cssKey: '--iw-fs-micro', dartExpr: 'PmonText.micro', kind: 'derived'), + (cssKey: '--iw-fs-metric', dartExpr: 'InkwatchText.metric', kind: 'derived'), + (cssKey: '--iw-fs-title', dartExpr: 'InkwatchText.title', kind: 'derived'), + (cssKey: '--iw-fs-stat', dartExpr: 'InkwatchText.stat', kind: 'derived'), + (cssKey: '--iw-fs-section', dartExpr: 'InkwatchText.section', kind: 'derived'), + (cssKey: '--iw-fs-body', dartExpr: 'InkwatchText.body', kind: 'derived'), + (cssKey: '--iw-fs-caption', dartExpr: 'InkwatchText.caption', kind: 'derived'), + (cssKey: '--iw-fs-micro', dartExpr: 'InkwatchText.micro', kind: 'derived'), // ── Web 专属(05 §3.3):CSS 有、Dart 不补;dartExpr 是「应缺席」的名字 ── - (cssKey: '--iw-surface-hover', dartExpr: 'PmonColors.surfaceHover', kind: 'webOnly'), - (cssKey: '--iw-line-strong', dartExpr: 'PmonColors.lineStrong', kind: 'webOnly'), - (cssKey: '--iw-text-on-accent', dartExpr: 'PmonColors.textOnAccent', kind: 'webOnly'), - (cssKey: '--iw-primary-hover', dartExpr: 'PmonColors.primaryHover', kind: 'webOnly'), - (cssKey: '--iw-primary-active', dartExpr: 'PmonColors.primaryActive', kind: 'webOnly'), - (cssKey: '--iw-info-border', dartExpr: 'PmonColors.infoBorder', kind: 'webOnly'), - (cssKey: '--iw-online-border', dartExpr: 'PmonColors.onlineBorder', kind: 'webOnly'), - (cssKey: '--iw-suspect-border', dartExpr: 'PmonColors.suspectBorder', kind: 'webOnly'), - (cssKey: '--iw-offline-border', dartExpr: 'PmonColors.offlineBorder', kind: 'webOnly'), - (cssKey: '--iw-unknown-border', dartExpr: 'PmonColors.unknownBorder', kind: 'webOnly'), - (cssKey: '--iw-danger-hover', dartExpr: 'PmonColors.dangerHover', kind: 'webOnly'), - (cssKey: '--iw-danger-active', dartExpr: 'PmonColors.dangerActive', kind: 'webOnly'), - (cssKey: '--iw-danger-border', dartExpr: 'PmonColors.dangerBorder', kind: 'webOnly'), - (cssKey: '--iw-chart-area', dartExpr: 'PmonColors.chartArea', kind: 'webOnly'), - (cssKey: '--iw-chart-grid', dartExpr: 'PmonColors.chartGrid', kind: 'webOnly'), - (cssKey: '--iw-chart-axis', dartExpr: 'PmonColors.chartAxis', kind: 'webOnly'), - (cssKey: '--iw-chart-gap-band', dartExpr: 'PmonColors.chartGapBand', kind: 'webOnly'), - (cssKey: '--iw-chart-threshold-band', dartExpr: 'PmonColors.chartThresholdBand', kind: 'webOnly'), - (cssKey: '--iw-chart-threshold-line', dartExpr: 'PmonColors.chartThresholdLine', kind: 'webOnly'), - (cssKey: '--iw-series-2', dartExpr: 'PmonColors.series2', kind: 'webOnly'), - (cssKey: '--iw-series-3', dartExpr: 'PmonColors.series3', kind: 'webOnly'), - (cssKey: '--iw-series-4', dartExpr: 'PmonColors.series4', kind: 'webOnly'), - (cssKey: '--iw-logo-bg', dartExpr: 'PmonColors.logoBg', kind: 'webOnly'), - (cssKey: '--iw-logo-wave', dartExpr: 'PmonColors.logoWave', kind: 'webOnly'), - (cssKey: '--iw-logo-accent', dartExpr: 'PmonColors.logoAccent', kind: 'webOnly'), - (cssKey: '--iw-logo-mono', dartExpr: 'PmonColors.logoMono', kind: 'webOnly'), - (cssKey: '--iw-mask', dartExpr: 'PmonColors.mask', kind: 'webOnly'), - (cssKey: '--iw-snackbar-bg', dartExpr: 'PmonColors.snackbarBg', kind: 'webOnly'), + (cssKey: '--iw-surface-hover', dartExpr: 'InkwatchColors.surfaceHover', kind: 'webOnly'), + (cssKey: '--iw-line-strong', dartExpr: 'InkwatchColors.lineStrong', kind: 'webOnly'), + (cssKey: '--iw-text-on-accent', dartExpr: 'InkwatchColors.textOnAccent', kind: 'webOnly'), + (cssKey: '--iw-primary-hover', dartExpr: 'InkwatchColors.primaryHover', kind: 'webOnly'), + (cssKey: '--iw-primary-active', dartExpr: 'InkwatchColors.primaryActive', kind: 'webOnly'), + (cssKey: '--iw-info-border', dartExpr: 'InkwatchColors.infoBorder', kind: 'webOnly'), + (cssKey: '--iw-online-border', dartExpr: 'InkwatchColors.onlineBorder', kind: 'webOnly'), + (cssKey: '--iw-suspect-border', dartExpr: 'InkwatchColors.suspectBorder', kind: 'webOnly'), + (cssKey: '--iw-offline-border', dartExpr: 'InkwatchColors.offlineBorder', kind: 'webOnly'), + (cssKey: '--iw-unknown-border', dartExpr: 'InkwatchColors.unknownBorder', kind: 'webOnly'), + (cssKey: '--iw-danger-hover', dartExpr: 'InkwatchColors.dangerHover', kind: 'webOnly'), + (cssKey: '--iw-danger-active', dartExpr: 'InkwatchColors.dangerActive', kind: 'webOnly'), + (cssKey: '--iw-danger-border', dartExpr: 'InkwatchColors.dangerBorder', kind: 'webOnly'), + (cssKey: '--iw-chart-area', dartExpr: 'InkwatchColors.chartArea', kind: 'webOnly'), + (cssKey: '--iw-chart-grid', dartExpr: 'InkwatchColors.chartGrid', kind: 'webOnly'), + (cssKey: '--iw-chart-axis', dartExpr: 'InkwatchColors.chartAxis', kind: 'webOnly'), + (cssKey: '--iw-chart-gap-band', dartExpr: 'InkwatchColors.chartGapBand', kind: 'webOnly'), + (cssKey: '--iw-chart-threshold-band', dartExpr: 'InkwatchColors.chartThresholdBand', kind: 'webOnly'), + (cssKey: '--iw-chart-threshold-line', dartExpr: 'InkwatchColors.chartThresholdLine', kind: 'webOnly'), + (cssKey: '--iw-series-2', dartExpr: 'InkwatchColors.series2', kind: 'webOnly'), + (cssKey: '--iw-series-3', dartExpr: 'InkwatchColors.series3', kind: 'webOnly'), + (cssKey: '--iw-series-4', dartExpr: 'InkwatchColors.series4', kind: 'webOnly'), + (cssKey: '--iw-logo-bg', dartExpr: 'InkwatchColors.logoBg', kind: 'webOnly'), + (cssKey: '--iw-logo-wave', dartExpr: 'InkwatchColors.logoWave', kind: 'webOnly'), + (cssKey: '--iw-logo-accent', dartExpr: 'InkwatchColors.logoAccent', kind: 'webOnly'), + (cssKey: '--iw-logo-mono', dartExpr: 'InkwatchColors.logoMono', kind: 'webOnly'), + (cssKey: '--iw-mask', dartExpr: 'InkwatchColors.mask', kind: 'webOnly'), + (cssKey: '--iw-snackbar-bg', dartExpr: 'InkwatchColors.snackbarBg', kind: 'webOnly'), // 大屏专用字号,Flutter 端无大屏(05 §3.3) - (cssKey: '--iw-fs-display', dartExpr: 'PmonText.display', kind: 'webOnly'), + (cssKey: '--iw-fs-display', dartExpr: 'InkwatchText.display', kind: 'webOnly'), // ── Dart 专属:Flutter 有语义档位、CSS 刻意不设同名键 ── - (cssKey: '--iw-fs-body-muted', dartExpr: 'PmonText.bodyMuted', kind: 'dartOnly'), - (cssKey: '--iw-fs-metric-unit', dartExpr: 'PmonText.metricUnit', kind: 'dartOnly'), + (cssKey: '--iw-fs-body-muted', dartExpr: 'InkwatchText.bodyMuted', kind: 'dartOnly'), + (cssKey: '--iw-fs-metric-unit', dartExpr: 'InkwatchText.metricUnit', kind: 'dartOnly'), ]; /// 浅色 Web 专属键白名单(05 §3.3 / C-16)。 @@ -166,7 +166,7 @@ const Set kWebOnlyTokens = { '--iw-fs-display', }; -/// E-Ink 六档字阶:CSS 是 `PmonEinkText` 的镜像,纳入双向比对(05 §3.3) +/// E-Ink 六档字阶:CSS 是 `InkwatchEinkText` 的镜像,纳入双向比对(05 §3.3) const List<({String cssKey, String dartName})> kEinkFontParity = [ (cssKey: '--iw-eink-fs-value', dartName: 'value'), (cssKey: '--iw-eink-fs-glyph', dartName: 'glyph'), @@ -177,8 +177,8 @@ const List<({String cssKey, String dartName})> kEinkFontParity = [ ]; // ── 深色 parity(本阶段新增):58 键 = 转录 30 + 豁免 28 ───────────── -/// 转录项:CSS dark 块键 ↔ `PmonDarkColors` 常量名。 -/// 只列「Flutter 真的会消费」的键 —— 与浅色 PmonColors 同名语义,外加两处 +/// 转录项:CSS dark 块键 ↔ `InkwatchDarkColors` 常量名。 +/// 只列「Flutter 真的会消费」的键 —— 与浅色 InkwatchColors 同名语义,外加两处 /// D3 反相(text-on-accent / snackbar-bg);别名复用 /// (offline-soft / unknown-soft → surfaceMuted、unknown → offline、 /// series-1 → chartLine)与浅色侧 `kTokenParity` 的写法一致。 @@ -216,7 +216,7 @@ const List<({String cssKey, String dartName})> kDarkTranscribed = [ ]; /// 豁免项:dark 块里**未转录**的 Web 专属键(05 §1.4 / 浅色 kWebOnlyTokens -/// 同一纪律)。逐项登记、Dart 侧保持缺省,理由见 `theme.dart` 的 PmonDarkColors +/// 同一纪律)。逐项登记、Dart 侧保持缺省,理由见 `theme.dart` 的 InkwatchDarkColors /// 文件头:交互态用 `WidgetState`、描边用 `withValues(alpha:)` 动态算、 /// 多序列 / Logo / 预混面积在 Flutter 端没有消费点。 const Set kDarkExemptTokens = { @@ -259,15 +259,15 @@ late Map cssCore; // 第一个 :root(浅色 Core) late Map cssEngineering; // 第二个 :root(工程化补充) late Map cssDark; // html[data-iw-theme="dark"] 块 late String cssClean; -late Map dartColors; // PmonColors: name → FFRRGGBB -late Map darkColors; // PmonDarkColors: name → FFRRGGBB -late Map einkColors; // PmonEink: name → FFRRGGBB -// ⚠ PmonColors.page 与 PmonEink.page 同名,必须按 class 分段解析, +late Map dartColors; // InkwatchColors: name → FFRRGGBB +late Map darkColors; // InkwatchDarkColors: name → FFRRGGBB +late Map einkColors; // InkwatchEink: name → FFRRGGBB +// ⚠ InkwatchColors.page 与 InkwatchEink.page 同名,必须按 class 分段解析, // 否则后者会覆盖前者,`--iw-page` 会被误判成 #FFFFFF 漂移。 late Map dartAliases; // name → 另一个 name(如 chartLine → primary) -late Map dartNumbers; // PmonSpace / PmonRadius -late Map dartFontSizes; // PmonText -late Map einkFontSizes; // PmonEinkText +late Map dartNumbers; // InkwatchSpace / InkwatchRadius +late Map dartFontSizes; // InkwatchText +late Map einkFontSizes; // InkwatchEinkText late String dartClean; Map _parseKv(String body) { @@ -330,11 +330,11 @@ String normalizeCss(String raw) { String? resolveDart(String expr) { if (expr.isEmpty) return null; final name = expr.split('.').last; - final color = expr.startsWith('PmonEink.') + final color = expr.startsWith('InkwatchEink.') ? einkColors[name] : (dartColors[name] ?? dartColors[dartAliases[name]]); if (color != null) return color; - if (expr.startsWith('PmonEinkText.')) return einkFontSizes[name]; + if (expr.startsWith('InkwatchEinkText.')) return einkFontSizes[name]; final n = dartNumbers[name]; if (n != null) return _num(n); final fs = dartFontSizes[name]; @@ -342,9 +342,9 @@ String? resolveDart(String expr) { return null; } -/// 深色侧专用:只查 PmonDarkColors。 +/// 深色侧专用:只查 InkwatchDarkColors。 /// ⚠️ 两套色板字段名同名(page / surface / primary…),**绝不能**复用 -/// [resolveDart] —— 那样会取到浅色 PmonColors 的值,深色 parity 会假通过。 +/// [resolveDart] —— 那样会取到浅色 InkwatchColors 的值,深色 parity 会假通过。 String? resolveDark(String name) => darkColors[name] ?? darkColors[dartAliases[name]]; @@ -401,9 +401,9 @@ void main() { final dartSrc = File(dartPath).readAsStringSync(); expect(dartSrc.length, greaterThan(200), reason: '$dartPath 读不到内容?'); dartClean = dartSrc.replaceAll(RegExp(r'//[^\n]*'), ''); - dartColors = _parseColors(_classBody(dartClean, 'PmonColors')); - darkColors = _parseColors(_classBody(dartClean, 'PmonDarkColors')); - einkColors = _parseColors(_classBody(dartClean, 'PmonEink')); + dartColors = _parseColors(_classBody(dartClean, 'InkwatchColors')); + darkColors = _parseColors(_classBody(dartClean, 'InkwatchDarkColors')); + einkColors = _parseColors(_classBody(dartClean, 'InkwatchEink')); dartAliases = {}; for (final m in RegExp(r'static const (\w+)\s*=\s*(\w+);') .allMatches(dartClean)) { @@ -414,8 +414,8 @@ void main() { .allMatches(dartClean)) { dartNumbers[m.group(1)!] = m.group(2)!; } - dartFontSizes = _parseFontSizes(_classBody(dartClean, 'PmonText')); - einkFontSizes = _parseFontSizes(_classBody(dartClean, 'PmonEinkText')); + dartFontSizes = _parseFontSizes(_classBody(dartClean, 'InkwatchText')); + einkFontSizes = _parseFontSizes(_classBody(dartClean, 'InkwatchEinkText')); }); group('① 双向值比对(只比浅色 Core)', () { @@ -505,7 +505,7 @@ void main() { }); group('③ E-Ink 字阶与深色边界', () { - test('--iw-eink-fs-* 六档与 PmonEinkText 逐档相等', () { + test('--iw-eink-fs-* 六档与 InkwatchEinkText 逐档相等', () { final offenders = []; for (final e in kEinkFontParity) { final cssVal = cssEngineering.containsKey(e.cssKey) @@ -514,11 +514,11 @@ void main() { final dartVal = einkFontSizes[e.dartName]; if (cssVal == null || dartVal == null || cssVal != _num(dartVal)) { offenders.add('${e.cssKey} css=$cssVal ' - 'PmonEinkText.${e.dartName} dart=$dartVal'); + 'InkwatchEinkText.${e.dartName} dart=$dartVal'); } } expect(offenders, isEmpty, - reason: 'E-Ink 字阶是 PmonEinkText 的镜像,必须逐档相等:\n' + reason: 'E-Ink 字阶是 InkwatchEinkText 的镜像,必须逐档相等:\n' '${offenders.join('\n')}'); expect(einkFontSizes.length, 6, reason: 'E-Ink 字阶必须是完整的 6 档'); }); @@ -533,13 +533,13 @@ void main() { // dark 块必须排在 E-Ink 块之前(G2 层叠顺序) expect(cssClean.indexOf('html[data-iw-theme="dark"]'), lessThan(cssClean.indexOf('html[data-iw-surface="eink"]'))); - // 深色**现在进 parity**(本阶段已落地 PmonDarkColors),逐项比对见 group ④。 - expect(dartClean.contains('PmonDarkColors'), isTrue); - expect(dartClean.contains('buildPmonDarkTheme'), isTrue); + // 深色**现在进 parity**(本阶段已落地 InkwatchDarkColors),逐项比对见 group ④。 + expect(dartClean.contains('InkwatchDarkColors'), isTrue); + expect(dartClean.contains('buildInkwatchDarkTheme'), isTrue); }); }); - group('④ 深色 parity:CSS dark 块 ↔ PmonDarkColors(双向)', () { + group('④ 深色 parity:CSS dark 块 ↔ InkwatchDarkColors(双向)', () { test('转录 + 豁免恰好覆盖 58 键,且都在 CSS dark 块里(数量一致)', () { final covered = { ...kDarkTranscribed.map((e) => e.cssKey), @@ -558,7 +558,7 @@ void main() { '${dangling.join('\n')}'); }); - test('转录项逐项值相等(CSS dark 覆写值 == PmonDarkColors)', () { + test('转录项逐项值相等(CSS dark 覆写值 == InkwatchDarkColors)', () { final offenders = []; var checked = 0; for (final e in kDarkTranscribed) { @@ -569,7 +569,7 @@ void main() { final dartVal = resolveDark(e.dartName); if (cssVal == null || dartVal == null || cssVal != dartVal) { offenders.add('${e.cssKey.padRight(30)} css=${cssVal ?? "<缺>"}' - ' PmonDarkColors.${e.dartName.padRight(16)} ' + ' InkwatchDarkColors.${e.dartName.padRight(16)} ' 'dart=${dartVal ?? "<缺>"}'); } } @@ -579,7 +579,7 @@ void main() { '${offenders.join('\n')}'); }); - test('反向缺项:PmonDarkColors 每个颜色常量都必须已登记', () { + test('反向缺项:InkwatchDarkColors 每个颜色常量都必须已登记', () { final registered = kDarkTranscribed.map((e) => e.dartName).toSet(); final missing = darkColors.keys .where((k) => !registered.contains(k)) @@ -598,7 +598,7 @@ void main() { } final camel = _kebabToCamel(k.replaceFirst('--iw-', '')); if (darkColors.containsKey(camel)) { - offenders.add('$k: PmonDarkColors 擅自补了 $camel'); + offenders.add('$k: InkwatchDarkColors 擅自补了 $camel'); } } expect(kDarkExemptTokens.length, 58 - kDarkTranscribed.length, diff --git a/rust-core/Cargo.lock b/rust-core/Cargo.lock index 0f75e89..4cab472 100644 --- a/rust-core/Cargo.lock +++ b/rust-core/Cargo.lock @@ -39,6 +39,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "inkwatch-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sysinfo", +] + [[package]] name = "itoa" version = "1.0.18" @@ -66,15 +75,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "inkwatch-core" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "sysinfo", -] - [[package]] name = "proc-macro2" version = "1.0.107" diff --git a/server/business/admin/assets/tokens.css b/server/business/admin/assets/tokens.css index 9e70f51..0b7c9c2 100644 --- a/server/business/admin/assets/tokens.css +++ b/server/business/admin/assets/tokens.css @@ -247,7 +247,7 @@ --iw-pairing-tracking: .12em; --iw-fs-pairing: 24px; /* §4.4 配对码展示档 24px/600 */ - /* E-Ink 字阶(§2.8 PmonEinkText,全分支最低 w600) */ + /* E-Ink 字阶(§2.8 InkwatchEinkText,全分支最低 w600) */ --iw-eink-fs-value: 40px; --iw-eink-fs-glyph: 22px; --iw-eink-fs-title: 20px;