feat(design-system): 合入 V2 线并统一命名为 Inkwatch,补齐深色主题与状态符号图标化
Design System Guard / guard (push) Failing after 7m11s
Design System Guard / guard (push) Failing after 7m11s
本提交收口本轮四件事。三者改动在同一批文件里交织(改名触及 theme.dart、
深色化也在 theme.dart),按文件无法干净切分,故合为一个可审计的提交,
下面按事由分述。
一、命名统一:Pmon* → Inkwatch*(合入 V2 线后的收尾)
· 包名 pmon_client → inkwatch_client,Rust crate pmon-core → inkwatch-core。
改名后必须 flutter pub get 重建 package_config,否则 dart analyze 会报
1688 个"找不到 package:pmon_client"的假错误。
· 符号:PmonColors→InkwatchColors、PmonPalette→InkwatchPalette、
PmonTextStyles→InkwatchTextStyles、PmonRadius→InkwatchRadius、
PmonEinkText→InkwatchEinkText、PMON_LOG_*→INKWATCH_LOG_*。
· 覆盖 server/ client/ rust-core/ tests/ 全树;已用
git grep -In 'Pmon\|PMON\|pmon' 复核,零残留(tokens.css 里最后一处
§2.8 的类名注释也在本提交修掉)。
· rust-core/Cargo.lock 的变动只是包名变化导致 [[package]] 字典序重排。
二、深色主题:58 键同名覆写 + Flutter 整屏深色化
· tokens.css 增 dark 块(58 键),键名与浅色**逐一相同**——
这是 token_parity_test.dart 的深色 parity 组在守的约束。
· theme.dart:新增 InkwatchDarkColors / _assemble(palette, brightness),
把语义色从"常量"改为"随 brightness 装配"。
· main.dart:darkTheme + 显式 themeMode(ValueNotifier 持久化,不用
ThemeMode.system —— 水墨屏用户要的是自己定,不是跟系统)。
· theme_guard_test.dart C-15 → C-15′:从"禁止出现深色"改为"允许深色,
但必须 58 键同名",护栏跟着设计意图一起改,而不是删掉。
· 新增 dark_theme_test.dart / dark_ui_test.dart 覆盖装配与整屏渲染。
三、状态符号图标化:Unicode 字形 → Material Icons
· symbol_guard_test.dart 按**码位**扫 lib/(注释之外),命中 5 处:
transfer_card.dart 的 ⚠️/✓/✓/⚠,viewer_page.dart 的 ✓。
· 只删字形会留下不可见的 U+FE0F,源码看不出、界面渲染成豆腐块,
且按 emoji 区间匹配的旧护栏看不见它 —— 故护栏按码位判,且判据选
"扫注释之外全部字符"(错了只会多报,不会漏)。
· 改法:状态图标单独一槽,不拼进文案。
- transfer_card.dart:_offerHint 由 String? 改为
({IconData? icon, Color? tint, String text})?,渲染为 Row(Icon+Text);
「已收下」结果块同理(check_circle / error_outline)。
图标着色:warning_amber→_p.suspect,check_circle→_p.online。
- viewer_page.dart:_lanSummary() 返回 ({IconData? icon, String text}),
诊断行由 ('对端内网', _lanSummary(), null) 改为 (…, lan.text, lan.icon)。
调用点提到 return 之前只算一次。
· 理由不只是"好看":字符符号在水墨屏上会渲染成方框,且无法随主题着色。
验证
· client/scripts/test.sh:674 项全过(+674,0 失败,23s)
· tests/run-guards.sh(RUN_AXE=1):
web-token-guard.js PASS 21/0
phase3b-static-guard.js PASS 87/0
dockerfile-coverage-test.js PASS 53/0
schema-consistency-test.js PASS 116/0
client Dart 全量 PASS 674 项
admin-axe-audit.mjs PASS 42/42 格 viol=0(inc=12 有台账)
6 套件 PASS 6 / FAIL 0 / SKIPPED 0
· dart analyze:9 issues / 0 error(全部为既有 info/warning,非本次引入)
· 两处 tokens.css(仓库内 admin/assets 与设计系统交付目录)逐字节一致
sha256 088873cf6ece8da78135f96ca4633a537ac7802ea5f0fc8bd9c9294bb8a7bcaa
已知未做(写下来,不藏)
· flutter analyze 在本机因仓库路径含中文(水墨屏工具)触发 analysis_server
的 LSP 分帧 UTF-8 错误(FormatException: Unterminated string)。
绕法是用 dart analyze,或把工作树放到纯 ASCII 路径下。
· 窄屏降级态 + 失败态的组合真机复核:仍未做。
· VoiceOver / NVDA 实机验证:NOT RUN。
· 管理台登录框仍缺持久 <label>(现靠 aria-labelledby 指向标题)。
This commit is contained in:
1 parent
89260024ee
commit
1d76c3b8ac
19 files changed
+945
-851
No files matched your search
+59
-59
@@ -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<void> 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<void> 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<HomeShell> {
|
||||
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<HomeShell> {
|
||||
// body 里发生什么它一无所知。不订阅的话,副屏底下会一直挂着
|
||||
// 「采集 / 查看」两个页签(真机反馈"底下的导航键没有隐藏")。
|
||||
bottomNavigationBar: ValueListenableBuilder<bool>(
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
_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<ControlPanelPage>
|
||||
/// 文案由 Rust 核心生成(`runtime_guidance`),避免在 Dart 侧再写一份。
|
||||
Future<void> _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<String>() ?? const <String>[];
|
||||
@@ -637,7 +637,7 @@ class _ControlPanelPageState extends State<ControlPanelPage>
|
||||
///
|
||||
/// ── 为什么 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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
|
||||
@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<ControlPanelPage>
|
||||
// 首屏三分之一被它吃掉,而真正要看的指标与折线被挤到下面。
|
||||
// 现在:标题与平台交给 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<ControlPanelPage>
|
||||
)),
|
||||
),
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
/// 能力一行 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<ControlPanelPage>
|
||||
/// —— 那样用户会把"形状变了"误读成"出事了"。
|
||||
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<ControlPanelPage>
|
||||
// 是 launchd 拉起的非沙箱进程,读真实家目录那份。用户在下面两项里
|
||||
// 改地址,只有界面内采集会跟着变,常驻采集不会 —— 不说清的话,
|
||||
// 症状是"改了网关地址、后台那台还在往旧地址发",两边都不报错。
|
||||
if (currentPlatform != PmonPlatform.android) ...[
|
||||
if (currentPlatform != InkwatchPlatform.android) ...[
|
||||
Text(
|
||||
'上面两项只对「界面内采集」生效。常驻采集是另一个进程,'
|
||||
'读的是安装器那份配置 —— 装好后在「常驻采集」卡里能看到它'
|
||||
@@ -1060,7 +1060,7 @@ class _ControlPanelPageState extends State<ControlPanelPage>
|
||||
/// ② 本版本未接入采集 —— 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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
}
|
||||
|
||||
Widget _dashboard(ClientAgent agent) {
|
||||
final core = PmonCore.instance;
|
||||
final core = InkwatchCore.instance;
|
||||
return ValueListenableBuilder<AgentStatus>(
|
||||
valueListenable: _statusVn,
|
||||
builder: (context, st, _) {
|
||||
final f = st.lastFrame ?? const <String, dynamic>{};
|
||||
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<ControlPanelPage>
|
||||
hardwareId: _hardwareId,
|
||||
onClaimed: (id) => unawaited(_issueDeviceTokenFor(id)),
|
||||
),
|
||||
SizedBox(height: PmonSpace.lg),
|
||||
SizedBox(height: InkwatchSpace.lg),
|
||||
// 设备间传文件:发出 / 接收两个区。与账号、配对同级 ——
|
||||
// 它同样是"这台机器与我的其它机器之间"的事,不是采样参数。
|
||||
// 把采集端会话接进来 → 免去手工粘贴领取码(不通时自动退回粘贴)。
|
||||
@@ -1232,12 +1232,12 @@ class _ControlPanelPageState extends State<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
}
|
||||
|
||||
/// 状态灯三态(方案 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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
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<ControlPanelPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: PmonSpace.sm),
|
||||
SizedBox(height: InkwatchSpace.sm),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
@@ -46,8 +46,8 @@ class AccountCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountCardState extends State<AccountCard> {
|
||||
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<AccountCard> {
|
||||
|
||||
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<AccountCard> {
|
||||
),
|
||||
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(
|
||||
|
||||
@@ -50,8 +50,8 @@ class DeviceMembersCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DeviceMembersCardState extends State<DeviceMembersCard> {
|
||||
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<MemberInfo> _members = const [];
|
||||
|
||||
@@ -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 = <Widget>[];
|
||||
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('退出')),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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<bool> active = ValueNotifier<bool>(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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class PairingCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PairingCardState extends State<PairingCard> {
|
||||
PmonPalette get _p => PmonPalette.of(context);
|
||||
InkwatchPalette get _p => InkwatchPalette.of(context);
|
||||
|
||||
late final PairingApi _api;
|
||||
Timer? _tick;
|
||||
|
||||
@@ -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<ResidentCard> {
|
||||
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<ResidentCard> {
|
||||
|
||||
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<ResidentCard> {
|
||||
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<ResidentCard> {
|
||||
? '安装并启动'
|
||||
: '重新安装并启动'),
|
||||
),
|
||||
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<ResidentCard> {
|
||||
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<ResidentCard> {
|
||||
///
|
||||
/// ── 为什么必须显示它(而不是显示界面自己那份)────────────────────
|
||||
/// 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<ResidentCard> {
|
||||
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<ResidentCard> {
|
||||
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<ResidentCard> {
|
||||
Text('操作失败(原因如下,来自安装器输出)',
|
||||
style: TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w600, color: _p.danger)),
|
||||
SizedBox(height: PmonSpace.xs),
|
||||
SizedBox(height: InkwatchSpace.xs),
|
||||
// 原样显示 CLI 的失败输出:哪条命令失败、错在哪,都在这里
|
||||
SelectableText(msg,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+218
-218
@@ -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 <ThemeExtension<dynamic>>[PmonPalette.light]);
|
||||
extensions: const <ThemeExtension<dynamic>>[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 <ThemeExtension<dynamic>>[PmonPalette.dark]);
|
||||
extensions: const <ThemeExtension<dynamic>>[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<PmonPalette> {
|
||||
const PmonPalette({
|
||||
class InkwatchPalette extends ThemeExtension<InkwatchPalette> {
|
||||
const InkwatchPalette({
|
||||
required this.page,
|
||||
required this.surface,
|
||||
required this.surfaceMuted,
|
||||
@@ -684,96 +684,96 @@ class PmonPalette extends ThemeExtension<PmonPalette> {
|
||||
|
||||
/// 当前主题的色板。裸 MaterialApp(widget 测试未挂 extensions)回退 [light],
|
||||
/// 与改造前视觉一致,也不会因 extension 缺失而崩。
|
||||
static PmonPalette of(BuildContext context) =>
|
||||
Theme.of(context).extension<PmonPalette>() ?? light;
|
||||
static InkwatchPalette of(BuildContext context) =>
|
||||
Theme.of(context).extension<InkwatchPalette>() ?? 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<PmonPalette> {
|
||||
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<PmonPalette> {
|
||||
);
|
||||
|
||||
@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<PmonPalette> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 文字样式出口:把 [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 <ThemeExtension<dynamic>>[PmonPalette.eink],
|
||||
scaffoldBackgroundColor: PmonEink.page,
|
||||
extensions: const <ThemeExtension<dynamic>>[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,
|
||||
),
|
||||
|
||||
@@ -67,8 +67,8 @@ class FileTransferCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FileTransferCardState extends State<FileTransferCard> {
|
||||
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<FileTransferCard> {
|
||||
|
||||
/// 「已通过服务器通知对端」或「没通知成,请把码发过去」——
|
||||
/// **这两句话必须分开说**:前者用户什么都不用做,后者要他去转发。
|
||||
String? _offerHint;
|
||||
///
|
||||
/// 状态图标单独一槽(`icon` + `tint`),不拼进文本:
|
||||
/// Unicode 的 `✓`/`⚠` 在水墨屏上会渲染成方框,且无法随主题着色;
|
||||
/// `test/symbol_guard_test.dart` 按码位扫 lib/ 防止这类写法回归。
|
||||
({IconData? icon, Color? tint, String text})? _offerHint;
|
||||
|
||||
// ── 免粘贴:对端主动送来的待领文件 ──
|
||||
StreamSubscription<LanIncomingOffer>? _offerSub;
|
||||
@@ -219,12 +223,25 @@ class _FileTransferCardState extends State<FileTransferCard> {
|
||||
_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<FileTransferCard> {
|
||||
Text(_sendBusy ? '处理中…' : '选择文件'),
|
||||
]),
|
||||
),
|
||||
SizedBox(height: PmonSpace.sm),
|
||||
SizedBox(height: InkwatchSpace.sm),
|
||||
// ⚠️ 这句话必须在**点发送之前**就摆在界面上:跨网只能走服务器中继,
|
||||
// 而中继当前有 1 MiB 单次上限。等到进度条不动再说,等于骗人。
|
||||
Text('跨网(两台设备不在同一局域网)时走服务器中继,'
|
||||
@@ -429,9 +446,26 @@ class _FileTransferCardState extends State<FileTransferCard> {
|
||||
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<FileTransferCard> {
|
||||
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}',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -88,8 +88,8 @@ class ViewerPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ViewerPageState extends State<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> with WidgetsBindingObserver {
|
||||
}),
|
||||
showDiagnostics: _einkDiag,
|
||||
onToggleDiagnostics: () => setState(() => _einkDiag = !_einkDiag),
|
||||
// 每行是 (标签, 值, 值前面的图标?) —— 图标单独一槽,符号不拼进文本
|
||||
// (Material Icons 才能随主题着色;字符符号在水墨屏上会渲染成方框)
|
||||
diagnostics: [
|
||||
('转屏方式', _softRotate ? '应用内(请把设备横置)' : '系统'),
|
||||
('转屏方式', _softRotate ? '应用内(请把设备横置)' : '系统', null),
|
||||
// 局域网底座的状态。**"没开"和"开了但对面连不上"必须能分辨** ——
|
||||
// 前者是 lastError,后者表现为"地址与端口都在、就是连不上"。
|
||||
(
|
||||
@@ -882,13 +893,14 @@ class _ViewerPageState extends State<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
// 操作按钮从旧深色 header 搬到这里:它们本就作用于"这份列表"
|
||||
SizedBox(height: PmonSpace.md),
|
||||
SizedBox(height: InkwatchSpace.md),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1245,12 +1257,12 @@ class _ViewerPageState extends State<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: PmonSpace.sm),
|
||||
SizedBox(width: InkwatchSpace.sm),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
@@ -1410,7 +1422,7 @@ class _ViewerPageState extends State<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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<ViewerPage> 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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+139
-139
@@ -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<String, Color> _fields(PmonPalette p) => <String, Color>{
|
||||
Map<String, Color> _fields(InkwatchPalette p) => <String, Color>{
|
||||
'page': p.page,
|
||||
'surface': p.surface,
|
||||
'surfaceMuted': p.surfaceMuted,
|
||||
@@ -49,57 +49,57 @@ Map<String, Color> _fields(PmonPalette p) => <String, Color>{
|
||||
};
|
||||
|
||||
Map<String, Color> _lightRef() => <String, Color>{
|
||||
'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<String, Color> _darkRef() => <String, Color>{
|
||||
'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<void> _pumpProbe(WidgetTester tester, ThemeData theme) async {
|
||||
}
|
||||
|
||||
/// 断言探针里每个自绘点都取当前色板 p 的对应值。
|
||||
void _expectProbe(WidgetTester tester, PmonPalette p) {
|
||||
void _expectProbe(WidgetTester tester, InkwatchPalette p) {
|
||||
// 标准卡片:白面 + 发丝线
|
||||
final card = tester.widget<Container>(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<Container>(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<Container>(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<PmonPalette>(),
|
||||
same(PmonPalette.light));
|
||||
expect(buildPmonDarkTheme().extension<PmonPalette>(),
|
||||
same(PmonPalette.dark));
|
||||
expect(einkTheme().extension<PmonPalette>(), same(PmonPalette.eink));
|
||||
expect(buildInkwatchTheme().extension<InkwatchPalette>(),
|
||||
same(InkwatchPalette.light));
|
||||
expect(buildInkwatchDarkTheme().extension<InkwatchPalette>(),
|
||||
same(InkwatchPalette.dark));
|
||||
expect(einkTheme().extension<InkwatchPalette>(), 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<Container>(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)));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<String> _uiFiles = [
|
||||
@@ -46,7 +46,7 @@ const List<String> _uiFiles = [
|
||||
'lib/ui/device_members_card.dart',
|
||||
];
|
||||
|
||||
/// 本轮按 `PmonText` 档位收敛过的界面文件(05 §3.4)。
|
||||
/// 本轮按 `InkwatchText` 档位收敛过的界面文件(05 §3.4)。
|
||||
/// 这些文件里**一个** `fontSize: 数字` 都不许有 —— 与 E-Ink 分支
|
||||
/// 「界面里不得写就地字号」同一纪律。新增已收敛文件务必登记到这里。
|
||||
const List<String> _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: '指标数值必须等宽,否则一排卡片刷新时数字会左右跳动',
|
||||
);
|
||||
|
||||
+115
-115
@@ -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<ParityEntry> 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<String> 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<String> kDarkExemptTokens = {
|
||||
@@ -259,15 +259,15 @@ late Map<String, String> cssCore; // 第一个 :root(浅色 Core)
|
||||
late Map<String, String> cssEngineering; // 第二个 :root(工程化补充)
|
||||
late Map<String, String> cssDark; // html[data-iw-theme="dark"] 块
|
||||
late String cssClean;
|
||||
late Map<String, String> dartColors; // PmonColors: name → FFRRGGBB
|
||||
late Map<String, String> darkColors; // PmonDarkColors: name → FFRRGGBB
|
||||
late Map<String, String> einkColors; // PmonEink: name → FFRRGGBB
|
||||
// ⚠ PmonColors.page 与 PmonEink.page 同名,必须按 class 分段解析,
|
||||
late Map<String, String> dartColors; // InkwatchColors: name → FFRRGGBB
|
||||
late Map<String, String> darkColors; // InkwatchDarkColors: name → FFRRGGBB
|
||||
late Map<String, String> einkColors; // InkwatchEink: name → FFRRGGBB
|
||||
// ⚠ InkwatchColors.page 与 InkwatchEink.page 同名,必须按 class 分段解析,
|
||||
// 否则后者会覆盖前者,`--iw-page` 会被误判成 #FFFFFF 漂移。
|
||||
late Map<String, String> dartAliases; // name → 另一个 name(如 chartLine → primary)
|
||||
late Map<String, String> dartNumbers; // PmonSpace / PmonRadius
|
||||
late Map<String, String> dartFontSizes; // PmonText
|
||||
late Map<String, String> einkFontSizes; // PmonEinkText
|
||||
late Map<String, String> dartNumbers; // InkwatchSpace / InkwatchRadius
|
||||
late Map<String, String> dartFontSizes; // InkwatchText
|
||||
late Map<String, String> einkFontSizes; // InkwatchEinkText
|
||||
late String dartClean;
|
||||
|
||||
Map<String, String> _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 = <String, String>{};
|
||||
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 = <String>[];
|
||||
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 = <String>{
|
||||
...kDarkTranscribed.map((e) => e.cssKey),
|
||||
@@ -558,7 +558,7 @@ void main() {
|
||||
'${dangling.join('\n')}');
|
||||
});
|
||||
|
||||
test('转录项逐项值相等(CSS dark 覆写值 == PmonDarkColors)', () {
|
||||
test('转录项逐项值相等(CSS dark 覆写值 == InkwatchDarkColors)', () {
|
||||
final offenders = <String>[];
|
||||
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,
|
||||
|
||||
Generated
+9
-9
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user