Update watch face: modular refactor + OWM/Solar/background + codegen tools

- Split render into FieldTable/Fields/Icons/Layout/Settings modules
- Add Fenix8V3Background, Owm (weather), Solar modules
- Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg)
- Tune README/manifest/strings/settings
This commit is contained in:
2026-09-10 07:10:55 +08:00
parent 52346bdd09
commit 4d96ab0912
23 changed files with 1549 additions and 344 deletions

View File

@@ -1,58 +1,92 @@
import Toybox.Lang;
import Toybox.Application;
// Every stored preference in one place, with the defaults that reproduce the
// design comps. Keys match resources/settings/properties.xml.
// ============================================================================
// 设置读取
// ----------------------------------------------------------------------------
// 所有存储的偏好设置集中在这里,默认值就是设计稿的那套配置。
// 键名与 resources/settings/properties.xml 一一对应。
//
// Properties.getValue returns null for a key the watch has not stored yet --
// a fresh install, or a setting added by an update -- so every read falls back
// to the default rather than trusting the stored value.
// 为什么要包一层而不是各处直接调 Properties.getValue
// Connect IQ 的 Properties.getValue 遇到「手表还没存过的键」时**不是返回 null
// 而是抛 Out of Bounds 异常**。每次版本更新新增设置项,在手表把新的
// properties.xml 合并进去之前都会撞上这个坑,轻则某个字段读不到,重则整个表盘
// 崩掉。所以全部读取都走本模块,异常一律降级成默认值。
// ============================================================================
module Settings {
// Field ids follow https://watchface.io/docs/datafields.
const DEF_LEFT_TOP = 712; // sunrise
const DEF_RIGHT_TOP = 639; // forecast high / low
const DEF_BOTTOM_LEFT = 3; // distance
const DEF_BOTTOM_RIGHT = 1; // steps
// ---- 四个数据位的默认字段 ----
// 字段编号沿用 https://watchface.io/docs/datafields与 Fields.mc 中一致。
// 这四个默认值合起来就是设计稿上的样子。
const DEF_LEFT_TOP = 712; // 左上:日出
const DEF_RIGHT_TOP = 639; // 右上:预报最高/最低气温
const DEF_BOTTOM_LEFT = 3; // 左下:距离
const DEF_BOTTOM_RIGHT = 1; // 右下:步数
// ------------------------------------------------------------------------
// 底层读取:吞掉「键不存在」异常,返回 null 交给上层兜底。
// 注意 catch 在这里是有效的Properties 抛的是普通异常);而 Background
// 模块的 Out of Bounds 属于 VM 级错误catch 不住,那边只能用别的办法绕开,
// 详见 Fenix8V3App.scheduleWeather()。
// ------------------------------------------------------------------------
function raw(key as String) {
try {
return Application.Properties.getValue(key);
} catch (e) {
return null;
}
}
// 读数字型设置,取不到就用 def。
function number(key as String, def as Number) as Number {
var v = Application.Properties.getValue(key);
var v = raw(key);
if (v == null) { return def; }
return v as Number;
}
// 读布尔型设置,取不到就用 def。
function bool(key as String, def as Boolean) as Boolean {
var v = Application.Properties.getValue(key);
var v = raw(key);
if (v == null) { return def; }
return v as Boolean;
}
// 读字符串型设置,取不到返回空串(不是 null省得每个调用点再判一次
function text(key as String) as String {
var v = Application.Properties.getValue(key);
var v = raw(key);
if (v == null) { return ""; }
return v.toString();
}
// Theme index, clamped into range in case a stored value outlives a
// release that removed a theme.
// ------------------------------------------------------------------------
// 主题序号0 基)。做范围钳制是为了防止这种情况:用户存的是「主题 7」
// 之后某个版本删掉了一套主题,存量值就会越界 —— 直接崩数组下标。
// ------------------------------------------------------------------------
function themeIndex(count as Number) as Number {
var i = number("Theme", 1) - 1;
if (i < 0 || i >= count) { return 0; }
return i;
}
// ---- 四个数据位选中的字段编号 ----
function leftTop() as Number { return number("LeftTop", DEF_LEFT_TOP); }
function rightTop() as Number { return number("RightTop", DEF_RIGHT_TOP); }
function bottomLeft() as Number { return number("BottomLeft", DEF_BOTTOM_LEFT); }
function bottomRight() as Number { return number("BottomRight", DEF_BOTTOM_RIGHT); }
function ringTR() as Number { return number("RingTR", 0); }
function ringTL() as Number { return number("RingTL", 1); }
function ringBR() as Number { return number("RingBR", 4); }
function ringBL() as Number { return number("RingBL", 3); }
// ---- 进度环四段各自绑定的指标 ----
// 注意这里的编号是进度环自己的一张短表0 步数 / 1 卡路里 / 2 距离 /
// 3 楼层 / 4 动动条 / 5 活动分钟 / 6 身体电量),与上面数据位的字段编号
// 是两套体系,不要混用。取值逻辑在 Fenix8V3View.ringProgress()。
function ringTR() as Number { return number("RingTR", 0); } // 右上:步数
function ringTL() as Number { return number("RingTL", 1); } // 左上:卡路里
function ringBR() as Number { return number("RingBR", 4); } // 右下:动动条
function ringBL() as Number { return number("RingBL", 3); } // 左下:楼层
// 温度单位0 跟随手表 / 1 强制摄氏 / 2 强制华氏。
function tempUnit() as Number { return number("TempUnit", 0); }
// ---- 三个显隐开关 ----
function showTop() as Boolean { return bool("ShowTop", true); }
function showBand() as Boolean { return bool("ShowBand", true); }
function showBottom() as Boolean { return bool("ShowBottom", true); }