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,12 +1,25 @@
import Toybox.Application;
import Toybox.Background;
import Toybox.Lang;
import Toybox.System;
import Toybox.Time;
import Toybox.WatchUi;
import Owm;
// Application shell. A watch face has no menus or input of its own, so this
// only hands back the view and forces a repaint when the wearer changes
// something in the Connect IQ settings.
// ============================================================================
// 应用入口
// ----------------------------------------------------------------------------
// 表盘没有菜单也拿不到输入,所以这个壳子只做三件事:
// 1. 交出 view
// 2. 设置变更时请求重绘
// 3. 管理拉取 OpenWeatherMap 的后台服务
// ============================================================================
class Fenix8V3App extends Application.AppBase {
// OWM 免费档的调用额度很宽裕,但表盘每分钟才重绘一次、数据本身也是小时级的,
// 拉太勤只是白白唤醒射频费电。半小时是个合适的折中。
const OWM_PERIOD_MIN = 30;
function initialize() {
AppBase.initialize();
}
@@ -17,15 +30,55 @@ class Fenix8V3App extends Application.AppBase {
function onStop(state as Dictionary?) as Void {
}
// ⚠️ 定时器的注册必须放在 getInitialView 里,不能放 onStart。
// 因为 onStart **在后台服务进程里也会被调用一次**,而在后台进程里调
// registerForTemporalEvent 会抛 Out of Bounds。
// getInitialView 只在前台进程运行,是安全的位置。
function getInitialView() as [Views] or [Views, InputDelegates] {
scheduleWeather();
return [new Fenix8V3View()];
}
// Fires when the phone pushes new settings down. The view reads every
// preference fresh on each draw, so a repaint is all that is needed.
function getServiceDelegate() as [System.ServiceDelegate] {
return [new Fenix8V3Background()];
}
// 只有用户真的填了 API key 才注册定时器 —— 没填就没什么可拉的,
// 注册了纯属浪费电。反过来,用户把 key 清空时也要及时注销。
function scheduleWeather() as Void {
if (!(Background has :registerForTemporalEvent)) { return; }
// ⚠️ deleteTemporalEvent 在「本来就没注册」时会抛 Out of Bounds
// 而这个错误是 VM 级的,**try/catch 抓不住**。
// 所以这里不去问系统当前状态,而是自己在 Storage 里记一个标志位。
// 重复注册本身是无害的(只是重置周期),所以只在状态真的变化时动手。
var want = Owm.enabled();
var have = Application.Storage.getValue("bgOn");
have = (have != null) && (have as Boolean);
if (want == have) { return; }
if (want) {
Background.registerForTemporalEvent(
new Time.Duration(OWM_PERIOD_MIN * 60));
} else {
Background.deleteTemporalEvent();
}
Application.Storage.setValue("bgOn", want);
}
// 手机把新设置推下来时触发。
// view 每次绘制都是现读设置,所以重绘一下就够了;
// 但天气定时器要跟着新的 key 状态启停,所以先过一遍 scheduleWeather。
function onSettingsChanged() as Void {
scheduleWeather();
WatchUi.requestUpdate();
}
// 后台服务把结果通过这里交回前台。存进 Storage 供表盘读取,然后重绘。
function onBackgroundData(data as Application.PersistableType) as Void {
if (data != null) {
Application.Storage.setValue(Owm.STORE, data);
WatchUi.requestUpdate();
}
}
}
function getApp() as Fenix8V3App {