烹饪目标设定页:12 种肉类硬编码默认值 + 4 个用户自定义槽(温度 + 名称 + 颜色,4ed9630 / TAPD #1003090 一轮 polish 起 3→4 槽 + 圆形 tile + 9 色 palette + 重命名能力)+ 早期警告距离三档(关 / -5°C / -10°C)。f7af51f 起本页是「改目标」两步流的第 1 步——tap protein / 确认 custom 槽就 pop 回调(不再有 Set Goal 中转按钮),由 cooking_page._navigateToMeatPresets chain 到 预设页 用该肉种的 doneness roller 微调,Save 时 (meatType, targetTempF) 原子落盘;early-warn chip 自 8513d11 / TAPD #1003234 起 tap 即时 commit 到全局 provider + 当前 session + pending params,不再参与 Save 的原子组合。
lib/features/thermometer/presentation/pages/meat_presets_page.dart(559 行)temperatureUnitProvider(显示单位)、earlyWarnLevelProvider(早期警告距离)、customSlotsProvider(4 个自定义槽,4ed9630 / TAPD #1003090 起 List<CustomSlot> 携 tempF / colorValue / name 三元组,JSON 持久化 custom_goal_slots_v2 键 + 一次性从 legacy custom_goal_slots_f 温度-only StringList 迁移;customSlotCount 由 3 升到 4)QuickTargetSheet(自定义槽弹窗用,与 cooking 页同款)/meat-presets(MeatPresetsPage.routeName)lib/app.dart 静态 routes map顶部返回 + 居中标题 "Goal Setting"。下方第一块是「Early Warning」卡片:图标 + 标题 + 三个 chip(Off / 5°C 或 9°F / 10°C 或 18°F,按当前单位显示),命中当前 level 的 chip 高亮橙色(primary)。下方是 4 列网格的 protein tile(圆形彩色背景 + SVG 图标 + 名称,名称字号 15pt per TAPD #1003060 / f7af51f):legacy _legacyProteins 12 项 / CM4 _cm4Proteins 8 项。⚠️ 28c930e / TAPD #1003090 起 Custom 槽脱出 protein grid(partial 末行 + 空 4th cell 看起来左对齐松散),先做成 居中 Row 一行 lime-绿圆角矩形。4ed9630(同 TAPD)再做一轮 polish:legacy customSlotCount 由 3 升到 4 与 4 列 protein grid 一一对齐,_CustomizeSlot 改为 圆形 64 px tile 复刻 protein 的形状,放回独立的 4 列 GridView 段(CM4 1 槽仍居中、用同一列宽);未设的槽是中性灰 +(_emptyFill = 0xFF3A3A3C),点开 QuickTargetSheet 顶部内嵌的 _SlotMetaEditor(名字字段 12 字符上限 + 9 色无绿 _slotPalette palette + 温度 picker)一次性设 temp + colorValue + name 三元组;已设的槽用用户挑色 + 温度做底,label 显用户名(缺则 Custom N),前景按 luminance flip 黑/白保证对比度;新槽按 index 取 _slotPalette seed 颜色。lime-绿独占外观已退役——颜色由用户决定。f7af51f 起无底部按钮——本页改为 tap-to-advance:tile 不再有选中态高亮 / 边框 / 阴影动画,tap 即触发 pop。
IconButton.onPressed → Navigator.pop(context)_EarlyWarnChip.onTap → _setEarlyWarnLevel(X)(X = 0 / 1 / 2)setState 翻 _earlyWarnLevel + 高亮切换 chipearlyWarnLevelProvider(全局,alarm-eval fallback + 页面种子);若 caller 透了 deviceId / probeNumber(cooking 页 / Adjust 嵌套路径),同时调 pendingCookParamsProvider((deviceId, pn)).notifier.setEarlyWarnLevel(level) + cookingSessionsProvider.notifier.updateEarlyWarnLevel(deviceId, pn, level),alarm evaluator 下个 eval tick 即按新偏移触发;打 EARLY_WARN_SET user-action DevLog。命名路由(app.dart)入口无 context,只 commit 全局 provider,宿主 cooking 页用 _syncEarlyWarnFromGlobal 在 grid 返回后兜底回写本页 state 与 per-probe stores。chip 不再随 protein-tile pick result 返回——8513d11 之前翻 10°C 再退出 grid 会丢选择(QA repro:再开页显示 5°C;active cook 仍以旧偏移触发,因 session.earlyWarnLevel 未变,TAPD #1003091 evaluator 的 canonical per-probe 源)。_ProteinTile.onTap → selectionClick + Navigator.pop(context, {meatType, targetTempF, isCustom: false})(8513d11 / TAPD #1003234 起 earlyWarnLevel key 从 pop result 移除,chip 在 grid 内 tap 即时 commit)_navigateToMeatPresets),caller 收到后再推 预设页 用该 meat 的 doneness roller seed_ProteinTile.onTap → HapticFeedback.selectionClick
→ Navigator.pop(context, {
meatType: proteins[i].name,
targetTempF: proteins[i].defaultTempF,
isCustom: false,
})
_CustomizeSlot.onTap(空或已设都 OK)→ _openCustomPicker(c, useCelsius)QuickTargetSheet,4ed9630 / TAPD #1003090 起 sheet 顶端嵌 _SlotMetaEditor(名字 TextField 12 字符上限 + 9 色 _slotPalette palette),下方仍是原温度 picker;温度初值取该槽已存温度(无则 122°F = 50°C _defaultCustomTempF),name 初值取槽现有 name(缺则空、hint 显 Custom N),color 初值取槽 colorValue(缺则 _slotPalette[slotIndex] seed)。用户改完 + Confirm 后 sheet 自身 pop,本页 onChanged(v) 调 customSlotsProvider.notifier.updateSlot(idx, tempF: v, colorValue: editColor.toARGB32(), name: editName.trim() or null) 把三元组一次性落盘 + 缓存 confirmed = v,sheet pop 后本页立刻 Navigator.pop(context, {meatType: <用户名 or 'Custom N'>, targetTempF: confirmed, isCustom: true})(8513d11 / TAPD #1003234 起 earlyWarnLevel key 同 protein-tile pop 一并移除);用户在 sheet 外点 dismiss 不会触发 onChanged,confirmed == null、本页留在 grid。isCustom: true 关键:一个改名为 "Beef" 的槽也走 MeatType.custom(CM4 wire E=1,遵 App 温度),不被误读为固件 Beef 预设——cooking_page._resolveMeatType 头加 if (_isCustomPreset) return MeatType.custom; 早返,详见 烹饪 Action #5/#6。_CustomizeSlot.onTap → _openCustomPicker(slotIndex, useCelsius)
→ showModalBottomSheet(QuickTargetSheet(
initial: slot.tempF ?? _defaultCustomTempF,
header: _SlotMetaEditor(initialName, hintName, palette, ...),
onChanged: (v) => {
customSlotsProvider.notifier.updateSlot(
slotIndex,
tempF: v,
colorValue: editColor.toARGB32(),
name: editName.trim() or null);
confirmed = v;
}))
→ await pop → if confirmed != null:
Navigator.pop(context, {
meatType: editName.trim() or 'Custom ${slotIndex + 1}',
targetTempF: confirmed,
isCustom: true,
})
customSlotsProvider 写入新三元组(JSON 持久化到 SharedPreferences custom_goal_slots_v2);本页 pop;caller 收到 result(含 isCustom: true)后再 chain Adjust,isCustom 通过 PresetPage.initialIsCustom 透到 cooking_page._isCustomPreset 再透到 PendingCookParams.isCustom / CookingSession.meatType == MeatType.custom_CustomizeSlot.onLongPress(仅当槽非空时启用) → _confirmClearCustom(slotIndex)customSlotsProvider.notifier.clearSlot(slotIndex)(4ed9630 / TAPD #1003090 起 setter 拆为 updateSlot(idx, tempF, colorValue, name) 三元组写 + clearSlot(idx) 回 empty + 态;旧 setSlot(idx, null) 已下线),grid 通过 watch(customSlotsProvider) 自动 rebuild 回 "+" 空态Navigator.pop(ctx, bool),无独立副作用按钮 + hasSelection / _selectedProteinIndex / _selectedCustomIndex 状态机一并删除(f7af51f)。改目标的唯一入口是上面 Action #3 / #4 的 tap-to-advance——选择即返回,无中转 confirm 步骤。详见 §Per spec 2026-05-26 tap-to-advance UX。
watch:temperatureUnitProvider(显示单位)、customSlotsProvider(自定义槽列表,持久化变更触发重建)。read:earlyWarnLevelProvider(chip seed 兜底——8513d11 / TAPD #1003234 起若 caller 透 deviceId / probeNumber,initState 种子改由 active CookingSession.earlyWarnLevel → pendingCookParamsProvider((deviceId, pn)).earlyWarnLevel 二级回落,再退到全局 provider;命名路由无 context 时直接读全局 provider)。write:chip tap 经 _setEarlyWarnLevel 即时写 earlyWarnLevelProvider,有 context 时同时写该探针的 pendingCookParamsProvider + cookingSessionsProvider(详见 §用户能做的操作 Action #2)。
无显著差异。
_legacyProteins 12 项 / _cm4Proteins 8 项):温度值与图标都是常量。新增肉种需要改源码 —— 可考虑搬到 ARB 或单独 config 文件。Icons.set_meal 给鱼,Icons.sailing 给龙虾)——视觉效果一般help_previews.dart)映射一致;_ProteinInfo.icon(IconData)字段随之改名为 glyph(String)。⚠️ c4fdd7b / TAPD #1003208 起 Goose / Ostrich 改 SVG 资源 —— 🪿 Emoji 15.0 / 🦤 Emoji 13.0 在老 Android emoji 字体(≤ Emoji 11.0)上渲染 tofu bars(鸵鸟根本无 Unicode emoji),改为内置 assets/icons/goose.svg / assets/icons/ostrich.svg 由 flutter_svg 以 40 px 渲染(与 28pt emoji 同光学尺寸)。Lobster tile 圆背景同次由红 0xFFCC3030 换 teal 0xFF2E7D72——原红圆吃掉 🦞 红 emoji(Kevin 2026-06-10)。⚠️ e1fcc84 / TAPD #1003220 起全部 12 个 emoji 改 hand-drawn SVG —— vendor emoji 字体在 Android 上不系统统一(Xiaomi / Google / Samsung 各自带不同的 emoji 字体),同一 codepoint 在不同机型画的图不同,是 1003208 已落 SVG 的 Goose / Ostrich 之外唯一测试机型间一致的;本 commit 把所有 12 个肉种都迁到 assets/icons/<name>.svg(beef / chicken / duck / fish / ham / hamburger / lobster / pork / sheep / turkey / veal / venison + 复用 1003208 的 goose / ostrich)。新增单一共享 map lib/core/utils/meat_icons.dart(meatIconAsset(String name) → String)给 legacy MeatPresetsPage 12-protein grid、CM4 _cm4Proteins 8-protein grid、help_previews.dart Meat grid 三处同源消费——杜绝 376d6fd 注的 "help-vs-live divergence" 再发生(finding 3)。Lamb + Mutton 共用 sheep.svg(两者都渲 羊肉);Veal 是 calf 头、Beef 是整头牛,刻意区分两个 牛 target。_ProteinInfo.glyph 字段在 legacy + help-previews 两份都拆掉,tile 渲染时按 meat name 从共享 map 查图。tools/icon_preview/meat_icons_preview.html 是 contact sheet 审稿工具(app-size grid + 2.5× 放大)。Per spec 2026-05-08(TAPD 报告):Goal Setting 页的 12 个肉种卡片不论 App 语言一直显示英文,外圈 chrome(页头、对话框、自定义槽 label)已正确翻译;同 bug 出现在烹饪页 Meat 磁贴、Dashboard active-cook chip、Cook Log 卡片副标题、Cook Log 详情页、以及 Preset 页 "Presets & Goals" 按钮 label。修复加 lib/core/utils/meat_localization.dart 一个共享 helper localizedMeatName(AppLocalizations l, String name)——按英文 identifier switch 返回当前 locale 字符串(en/zh/de/es/fr/it 6 语 ARB key 早就齐了,只是没接),未知 name 回退原字符串。英文 identifier 仍是存储/持久化/_resolveMeatType 匹配的 canonical 值,翻译只发生在 6 个渲染点:本页 12-protein grid(meat_presets_page.dart:412)、cooking_page.dart:659、cook_log_page.dart:167-169、cook_log_detail_page.dart:88-90、dashboard_page.dart:1074、preset_page.dart:323。Doneness.displayName + iOS Live Activity 的 meatType 显示仍是英文,留作 follow-up。Doneness 部分关闭(ffec9b8 / TAPD #1003107):cook-log 列表卡片副标题 + 详情页 Meat tile 行的 doneness 渲染从 .displayName 改为 localizedDoneness(l, session.doneness),同次新增 localizedCookStatus(l, CookSessionStatus) helper 闭合 status 徽章 i18n(详见 10-烹饪日志 §已知问题);Doneness.displayName getter 本身保留作 dev-log 用,但 cook-log 两处 UI 调用点不再经过。⚠️ 15e7e6d / TAPD #1003132 进一步换源:cook-log 列表 + 详情页同两个渲染点再从 localizedDoneness(l, session.doneness) 换到 localizedDonenessKey(l, donenessKeyForMeatTempF(session.displayMeatName, session.targetTemp, family: session.deviceId.boosterFamily))——原因是 session.doneness 在 cooking_page._toggleSession 启动会话时硬编码 Doneness.medium、永不动,所以所有 cook-log 行都显示 "Medium"。新 helper donenessKeyForMeatTempF(meat_doneness_tables.dart)按肉的 band 表 + 用户设定的 target 反查 doneness key:温度在 band 内直接 hit、落 gap 或越界取最近 edge band、平手取靠前一档;这也让 Chicken 的 USDA Crispy 174–179°F band 能渲染出来(5 项 Doneness enum 无此值)。该派生在显示时进行,已落 cook-log 的历史会话同步生效。iOS Live Activity 的 meatType / doneness 仍是英文 displayName 渲染,没在本次 commit 范围内、follow-up 仍开。
Per Liang TAPD 2026-05-09(log culinatech_log_20260509145619.txt,e024ef4):picker 返回的 meatType 字符串必须经 CookingSession.presetName 透到下游渲染,否则非 firmware-mapped 的预设会塌成 "Custom"。MeatType enum 只有 9 个 firmware 映射值(beef..venison + custom,firmwareValue 1-9),但本页 _proteins 12 项里 Mutton / Goose / Lobster / Ostrich / Ham 不映射,"Custom 1/2/..." 槽也不映射;cooking_page._resolveMeatType() 把这些落到 MeatType.custom 后,下游读 session.meatType.displayName 就一律显示 "Custom"——log 复现路径:picker 选 Mutton → Set Goal → 烹饪页正确显示 "Mutton" → Start → Dashboard chip 变 "Custom - Medium" → 重进烹饪页 tab 再退化为 "Custom"。修复加 CookingSession.presetName: String? 字段携带 picker 返回的原始 label(与上面 i18n note 描述的英文 canonical identifier 同源),新增 getter displayMeatName ⇒ presetName ?? meatType.displayName 作为 Dashboard chip / Cook Log 副标题 / Cook Log 详情 / Live Activity payload / 烹饪页 tab restore 五处的统一入口;session.meatType 仍是 firmware enum(写设备用),Cook Log "hide bare custom" 规则加 presetName == null 守卫——这样 "Mutton" / "Custom 1" 照常渲染,只有真正没绑预设标签的纯温度自定义才隐藏副标题。presetName 字段是 nullable 且 fromJson 缺键回 null,旧持久化会话照常解码,渲染回退到 meatType.displayName 与改前同。
Per Liang 2026-05-21(999555f):CM4 中继盒屏幕自带 8 肉种 + 1 自定义的目标设定列表,与 wire E 字段 2–9 一一对应(Veal/Beef/Chicken/Lamb/Hamburger/Pork/Fish/Turkey),App 端肉类 picker 必须与中继盒屏幕镜像对齐;legacy(CM1/CM2/CM3/MW3-5)wire 不携带肉种、超出 firmware 映射的标签是纯 App-side 文案,原 12 + 3 栅格保留不变(4ed9630 / TAPD #1003090 之后 legacy customSlotCount 升到 4,栅格变 12 + 4)。MeatPresetsPage / PresetPage 新增 family 入参(默认 BoosterFamily.legacy 兼容 app.dart 命名路由),cooking_page._navigateToAdjust / _navigateToMeatPresets 通过 widget.deviceId.boosterFamily 透传,运行时按 family.isCm4 切换常量列表 _cm4Proteins / _legacyProteins 与自定义槽数(CM4=1,legacy=customSlotCount=4)。该收窄是 TAPD #1003050 修复 #2(App→Booster 当前回显 "Custom" 而非选中名称)的前置——picker 收紧后每个选择都拥有 firmware-known E 值,后续在 setProbeTarget 里塞 meatType: resolved.cm4WireE 即可正确显示(bd5312e 已接线);#1(Booster→App RX 适配)那条独立路径由 40f3340 闭合数据链路(CM4_MEAT_SYNC_ADOPT 写入 CookingSession + pendingCookParamsProvider,详见 CM4 协议 §Q3),ded5d57 后再补 Dashboard 卡片 chip 的 pre-session 显示 fallback——之前 cookingMeat / cookingDoneness 仅从 session 取、温度 chip 已 mirror 到 pending.targetTempF fallback、肉种 chip 仍空白(TAPD #1003050 ① 报告的「温度 synced、肉类 still blank」非对称,BLE log culinatech_log_20260525141101 实证 ADOPT 13×/SKIP 0×、纯 App 显示 gap),改后 cookingMeat 走 pending.meatType fallback。⚠️ 531f0789(TAPD #1003112)随后把 Dashboard list-view 第 4 个 chip 从「meat 当 label / doneness 当 value」改为「固定 chipProtein label(en Meat(b70b4dc / TAPD #1003237,Per Liang 2026-06-16:原 en 文案 "Protein" 翻译不完整,与 zh 肉类 / de Fleisch / es·it Carne / fr Viande 五语「肉」对齐改为 "Meat";同 chipProtein chip 在烹饪页头、Dashboard cooking 卡片、localizedMeatName 的 no-meat-picked 通用兜底三处共享) / zh 肉类 / de Fleisch / es/it Carne / fr Viande)+ cookingMeat 当 value」,与 Target / Ambient / Time 三 chip 的 label+value 结构对齐;_DeviceCardData.cookingDoneness 字段与上面 donenessForTempF 派生已一并删除,dashboard list view 不再显示 doneness。Doneness 仍在烹饪页 / Cook Log / Live Activity 等处显示(详见 10-烹饪日志 的 Per spec 2026-05-08 follow-up)。
Per Liang screenshot 2026-05-22("字体小,稍微改大一些",d8178d8):4 列网格里 protein tile 与 Custom 槽下方的文字 label 字号由 11pt 提到 13pt——_ProteinTile 与 _CustomizeSlot 同步改 TextStyle.fontSize;childAspectRatio 0.85 + 64 px 圆形 + 4 px 间隙的 cell 垂直余量足够,不触发布局 overflow。
f7af51f 起把「Goal Setting → Set Goal」单步选择重构为「Goal Setting → Adjust」两步流——MeatPresetsPage 不再有底部 FilledButton 与 _selectedProteinIndex / _selectedCustomIndex 选中态机,tap protein / 确认 custom 槽即 Navigator.pop({meatType, targetTempF, isCustom}),由 cooking 页 _navigateToMeatPresets 接到结果后链式 push PresetPage(initialMeat, initialTargetTempF, family) 让用户在 meat 自己的 doneness roller + bands 上微调温度。meat / temp 在 Adjust 的 Save 时原子生效,任一页 back-dismiss 都不影响当前 goal。⚠️ 8513d11 / TAPD #1003234 起 early-warn 退出该原子组合——chip 在 grid 内 tap 即时 commit 到全局 provider + 当前 session + pending params,pickedWarn cooking 页缓存与 grid pop 的 earlyWarnLevel key 一并删除;详见 §用户能做的操作 Action #2。同 commit _ProteinTile.fontSize 13 → 15 pt(TAPD #1003060:Liang 实测 d8178d8 后仍偏小,15 在英文 "Hamburger" 4-col cell 仍单行不 wrap,~16 是 EN wrap 上限)。tile 选中态相关的 isSelected 参数 / AnimatedContainer border + boxShadow / FontWeight.w700 切换全部从 _ProteinTile + _CustomizeSlot 删除,cell 渲染收敛为常态 Container 一份外观。