diff --git a/README.md b/README.md
index caa4323..7213e0e 100644
--- a/README.md
+++ b/README.md
@@ -23,17 +23,30 @@ fenix8v3-watchface/
│ └── rebuild.py # 原作者从位图逆向出 SVG 的脚本(仅存档,不参与构建)
├── tools/
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
+│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
+│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
├── source/
│ ├── Fenix8V3App.mc # 应用入口
-│ ├── Fenix8V3View.mc # 全部渲染逻辑
-│ └── Themes.mc # ⚠️ 自动生成,勿手改
-├── resources/ # 英文文案 + 设置项定义
+│ ├── Fenix8V3View.mc # 渲染逻辑(含常亮模式)
+│ ├── Fields.mc # 75 项数据字段的取值与格式化
+│ ├── FieldTable.mc # ⚠️ 自动生成:字段 id 表
+│ └── Themes.mc # ⚠️ 自动生成:七套主题配色
+├── resources/ # 英文文案 + 设置项定义(部分自动生成)
├── resources-chn/ # 简体中文文案
+├── resources-icons-<宽度>/ # ⚠️ 自动生成:按屏幕尺寸光栅化的图标
+├── resources-launcher-<尺寸>/ # ⚠️ 自动生成:各尺寸启动图标
└── bin/ # 编译产物(已 gitignore)
```
-三份实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。
+重新生成全部派生文件:
+
+```bash
+python3 tools/gen_themes.py && python3 tools/gen_fields.py \
+ && python3 tools/gen_icons.py && python3 tools/gen_preview_svg.py
+```
+
+三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
---
@@ -127,42 +140,94 @@ python3 tools/gen_themes.py && python3 tools/gen_preview_svg.py
---
-## 7. 可配置设置
+## 7. 可配置设置与数据字段
-共 13 项,通过 Garmin Connect 手机 App 的「表盘设置」修改(定义在 `resources/settings/settings.xml`,默认值在 `properties.xml`)。
+19 项设置,在 Garmin Connect 手机 App 的「表盘设置」里改(定义在 `resources/settings/settings.xml`,默认值在 `properties.xml`,两者由 `tools/gen_fields.py` 生成)。
-| 设置 | 键 | 可选值 | 默认 |
-|------|----|--------|------|
-| 主题 | `Theme` | 1–7 | 1 |
-| 顶右 / 顶左 / 底右 / 底左弧指标 | `RingTR/TL/BR/BL` | 0 步数 / 1 卡路里 / 2 距离 / 3 楼层 / 4 动动条 | 0 / 1 / 4 / 3 |
-| 左上数据 | `LeftTop` | 见下 | 6(日出) |
-| 右上数据 | `RightTop` | 见下 | 8(最高/最低气温) |
-| 左下数据 | `BottomLeft` | 见下 | 3(距离) |
-| 右下数据 | `BottomRight` | 见下 | 2(步数) |
-| 温度单位 | `TempUnit` | 0 跟随手表 / 1 °C / 2 °F | 0 |
-| 显示顶行 / 日期带 / 底行 | `ShowTop` / `ShowBand` / `ShowBottom` | 布尔 | true |
+| 设置 | 键 | 默认 |
+|------|----|------|
+| 主题 | `Theme` | 1 (Ember) |
+| 四段进度环指标 | `RingTR/TL/BR/BL` | 步数 / 卡路里 / 动动条 / 楼层 |
+| 四个数据位 | `LeftTop` `RightTop` `BottomLeft` `BottomRight` | 日出 / 高低温 / 距离 / 步数 |
+| 温度单位 | `TempUnit` | 跟随手表 |
+| 第二时区 1 / 2 偏移(分钟) | `AltOffset1` `AltOffset2` | 0 |
+| 自定义文字 1 / 2 / 3 | `Custom1/2/3` | 空 |
+| 显示顶行 / 日期带 / 底行 | `ShowTop` `ShowBand` `ShowBottom` | true |
-**数据项**(四个数据位共用同一张表):`0` 当前气温 · `1` 卡路里 · `2` 步数 · `3` 距离 · `4` 楼层 · `5` 电量 · `6` 日出 · `7` 日落 · `8` 最高/最低气温 · `9` 心率。
+进度环指标:步数、卡路里、距离、楼层、动动条、活动分钟(对周目标)、身体电量。
-默认值即设计稿的配置:左上=日出时间、右上=`63°/52°`、左下=距离、右下=步数。
+### 数据字段
-底部两格的小标签跟随所选数据自动变化(`距离` / `步数` / `心率` …),不需要单独配置。
+四个数据位共用一张 **75 项** 的字段表,编号沿用 [watchface.io/docs/datafields](https://watchface.io/docs/datafields),两边可以直接对照。底部两格的小标签跟随所选字段自动变化,不需要单独配置。
+
+| 分类 | 字段 |
+|------|------|
+| 日期时间 | 时间、UTC 时间、第二时区 1/2、星期月日、星期日、月日、日、星期(简写/全称)、月份(名称/数字)、上午下午、周数(ISO / 日历) |
+| 活动 | 步数、步数取整、剩余步数、卡路里、活动卡路里、活动分钟(总/中强度/高强度/本周)、距离、上楼层数、下楼层数、爬升高度、推行距离、推行次数 |
+| 身体 | 心率、静息心率、压力指数、血氧、呼吸频率、身体电量、活动条(正/反向)、体重、BMI、恢复时间、最大摄氧量(跑/骑) |
+| 系统 | 电量、剩余天数、电量+天数、闹钟、通知、勿扰、蓝牙、太阳能强度 |
+| 环境 | 海拔、海平面气压、环境气压、日出、日落、下一太阳事件、距太阳事件、月龄、月球照明 |
+| 天气 | 气温、最高/最低、体感、最高、最低、湿度、降水概率、风速、风向角、风向、更新时间 |
+| 自定义 | 自定义文字 1 / 2 / 3 |
+
+加字段:在 `tools/gen_fields.py` 的 `FIELDS` 表里加一行,跑一遍生成器,再到 `source/Fields.mc` 的 `value()` 里补一个分支。设置项、两种语言的文案、表盘标签会自动跟上。
+
+### ⚠️ 没有实现的字段,以及原因
+
+watchface.io 的完整目录约 300 项,本表只收了 **手表本机能算出来的** 那些。其余几类不是没做,是一个自包含的 Connect IQ 表盘做不了:
+
+| 没做的 | 原因 |
+|--------|------|
+| OpenWeatherMap(约 30 项)、StormGlass.io(5 项) | 需要 `Communications.makeWebRequest` + 用户自备 API key + 后台服务定时拉取与缓存。是另一个子系统,且要求用户自己去注册 key |
+| 各运动项目的 WTD / 近 7 天 / 本月 / 近 28 天 汇总(约 80 项) | Connect IQ 不暴露按运动分类的历史累计。这些数据在 GreenBlack 那边是通过手机侧与 Garmin Connect 的集成同步来的,本机 API 只有 `ActivityMonitor.getHistory()` 的最近几天步数/卡路里/距离 |
+| 第三方 App 复杂功能(CGM、Hydration、QuoteGlance 等 12 项) | 需要用户装了对应的 App,并通过 Complications API 订阅 |
+| 日历事件、计时器、倒计时 | 日历不对 CIQ 开放;表盘拿不到触摸输入,没法做启停 |
+| 秒数 | 需要 `onPartialUpdate` 每秒重绘。技术上可行,但会明显吃电,暂未做 |
+| 晨昏蒙影、黄金/蓝调时刻(约 14 项) | 可以自己算太阳高度角推出来,只是还没写;月龄和月球照明已经是这样算的 |
+| 睡眠得分、训练状态 | `ActivityMonitor.Info` 上没有这两个字段(SDK 9.1.0 实测) |
+| 标记类字段(Marker,约 35 项) | 那是给刻度型表盘做环形标记用的,本表盘的进度环用的是自己的指标体系 |
---
-## 8. 数据来源
+## 8. 数据来源与权限
| 元素 | API |
|------|-----|
-| 步数 / 卡路里 / 距离 / 楼层 / 动动条 | `ActivityMonitor.getInfo()` |
-| 日出 / 日落 | `Weather.getSunrise/getSunset(location, Time.now())`,位置取 `CurrentConditions.observationLocationPosition`,回退 `Activity.getActivityInfo().currentLocation` |
-| 最高 / 最低气温 | `Weather.getDailyForecast()[0].highTemperature / lowTemperature` |
-| 当前气温 | `Weather.getCurrentConditions().temperature` |
-| 电量 | `System.getSystemStats().battery`(0–100 的百分比) |
-| 时间 / 日期 | `Time.Gregorian`,`FORMAT_SHORT`(只有短格式的 `day_of_week` / `month` 是数字) |
-| 蓝牙 | `DeviceSettings.phoneConnected`,未连接时图标压暗 |
+| 步数 / 卡路里 / 距离 / 楼层 / 动动条 / 活动分钟 / 呼吸 / 压力 / 恢复时间 | `ActivityMonitor.getInfo()` |
+| 心率 | `Activity.getActivityInfo().currentHeartRate`,回退 `ActivityMonitor.getHeartRateHistory()` |
+| 血氧 / 身体电量 | `SensorHistory`(需 `SensorHistory` 权限) |
+| 静息心率 / 体重 / 身高 / 最大摄氧量 | `UserProfile.getProfile()`(需 `UserProfile` 权限) |
+| 海拔 / 环境气压 / 海平面气压 | `Activity.getActivityInfo()` |
+| 日出 / 日落 | `Weather.getSunrise/getSunset(location, time)`,位置取 `CurrentConditions.observationLocationPosition`,回退 `Activity.currentLocation` |
+| 气温 / 体感 / 湿度 / 降水 / 风 | `Weather.getCurrentConditions()` |
+| 最高 / 最低气温 | `Weather.getDailyForecast()[0]` |
+| 电量 / 剩余天数 / 太阳能 | `System.getSystemStats()`(`battery` 是 0–100 的百分比,不是 0–1) |
+| 闹钟 / 通知 / 勿扰 / 蓝牙 / 单位制 | `System.getDeviceSettings()` |
+| 月龄 / 月球照明 | 自算(儒略日 + 朔望月 29.530588853 天) |
+| 周数 | 自算(ISO 8601 与日历周两种) |
-单位与制式跟随手表设置:`is24Hour`(24 小时制下不显示 AM/PM)、`distanceUnits`(km / mi)、`temperatureUnits`(°C / °F)。天气与日出需手表已配对并同步过数据,否则显示 `--`。
+### 权限
+
+`manifest.xml` 声明两项:`UserProfile`(静息心率、体重、VO2max)与 `SensorHistory`(血氧、身体电量)。用户安装时会看到授权提示。
+
+> `Sensor` 权限**不能**用于 `type="watchface"`(除非同时申请 `Background`),编译器会直接拒绝。所以海拔与气压走 `Activity.Info` 而不是 `Sensor.getInfo()`。
+
+单位与制式跟随手表:`is24Hour`、`distanceUnits`、`temperatureUnits`(温度可用 `TempUnit` 覆盖)。天气与日出需手表已配对并同步过数据,否则显示 `--`。
+
+---
+
+## 8.2 常亮显示(AOD)
+
+AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有烧屏风险,Garmin 上架审核也会卡。`onEnterSleep` / `onExitSleep` 切换到 `drawAmbient()`:
+
+- 不画日期带底色(整条亮橙色横幅是最费电的元素)
+- 进度环只画**点亮的**刻度,且整体压暗到 45%
+- 时间与日期压暗到 55%,其余数据位全部隐藏
+- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
+
+只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型(Fenix 7 / Enduro 3 等)不受影响,照常全量绘制。
+
+模拟器可用 `Settings → Display Mode` 切换验证。
---
diff --git a/manifest.xml b/manifest.xml
index e24ad13..c7cbb4c 100644
--- a/manifest.xml
+++ b/manifest.xml
@@ -34,5 +34,12 @@
eng
+
+
+
+
+
+
diff --git a/monkey.jungle b/monkey.jungle
index 19faf56..500909f 100644
--- a/monkey.jungle
+++ b/monkey.jungle
@@ -1,7 +1,32 @@
project.manifest = manifest.xml
base.sourcePath = source
-# resources-chn holds the Simplified Chinese variant of every string;
-# the -chn suffix is what tells monkeyc which language it belongs to.
+
+# resources-chn holds the Simplified Chinese variant of every string; the
+# -chn suffix is what tells monkeyc which language it belongs to.
base.resourcePath = resources;resources-chn
+
+# Two more resource trees per device, both generated by tools/gen_icons.py:
+# resources-icons- sunrise/sunset/weather rasterised from the
+# design paths for that exact pixel grid
+# resources-launcher- launcher icon at the size the device asks for
+
+enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
+fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fenix7s.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
+fenix7x.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+fenix8pro47mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+fr255.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fr265.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+fr965.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+venu3.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
+vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
+
base.excludeAnnotations = release
diff --git a/resources-chn/strings/fields.xml b/resources-chn/strings/fields.xml
new file mode 100644
index 0000000..bba8ac3
--- /dev/null
+++ b/resources-chn/strings/fields.xml
@@ -0,0 +1,89 @@
+
+
+ 关闭
+ 时间
+ UTC 时间
+ 第二时区 1
+ 第二时区 2
+ 星期 月 日
+ 星期 日
+ 月 日
+ 日
+ 星期简写
+ 星期全称
+ 月份
+ 月份数字
+ 上午/下午
+ 周数 (ISO)
+ 周数
+ 步数
+ 步数 (取整)
+ 剩余步数
+ 卡路里
+ 活动卡路里
+ 活动分钟
+ 中强度分钟
+ 高强度分钟
+ 本周活动分钟
+ 距离
+ 上楼层数
+ 下楼层数
+ 爬升高度
+ 推行距离
+ 推行次数
+ 心率
+ 静息心率
+ 压力指数
+ 血氧
+ 呼吸频率
+ 身体电量
+ 活动条
+ 活动条 (反向)
+ 体重
+ BMI
+ 恢复时间
+ 最大摄氧量 (跑)
+ 最大摄氧量 (骑)
+ 电量
+ 剩余天数
+ 电量 / 天数
+ 闹钟
+ 通知
+ 勿扰模式
+ 蓝牙
+ 太阳能强度
+ 海拔
+ 海平面气压
+ 环境气压
+ 日出
+ 日落
+ 下一太阳事件
+ 距太阳事件
+ 月龄
+ 月球照明
+ 气温
+ 最高/最低气温
+ 体感温度
+ 最高气温
+ 最低气温
+ 湿度
+ 降水概率
+ 风速
+ 风向角
+ 风向
+ 天气更新时间
+ 自定义文字 1
+ 自定义文字 2
+ 自定义文字 3
+
+
+ ,时间,UTC,时区1,时区2,日期,日期,日期,日,星期,星期,月,月,,周,周,步数,步数,还差,千卡,活动,分钟,中强度,高强度,周分钟,距离,楼层,下楼,爬升,推行,推行,心率,静息,压力,血氧,呼吸,体能,活动,活动,体重,BMI,恢复,摄氧,摄氧,电量,天数,电量,闹钟,通知,勿扰,蓝牙,太阳能,海拔,气压,气压,日出,日落,太阳,太阳,月龄,月相,气温,高低温,体感,最高,最低,湿度,降水,风速,风向,风向,更新,,,
+
+ 步数
+ 卡路里
+ 距离
+ 楼层
+ 动动条
+ 活动分钟
+ 身体电量
+
diff --git a/resources-chn/strings/strings.xml b/resources-chn/strings/strings.xml
index 299ef15..c94db93 100644
--- a/resources-chn/strings/strings.xml
+++ b/resources-chn/strings/strings.xml
@@ -10,10 +10,20 @@
右上数据
左下数据
右下数据
+ 温度单位
+ 第二时区 1(相对本地分钟数)
+ 第二时区 2(相对本地分钟数)
+ 自定义文字 1
+ 自定义文字 2
+ 自定义文字 3
显示顶部一行
显示日期带
显示底部一行
+ 跟随手表
+ 摄氏度 (°C)
+ 华氏度 (°F)
+
1 · 余烬
2 · 极光
3 · 黄铜
@@ -22,32 +32,9 @@
6 · 酸绿
7 · 海藻
- 步数
- 卡路里
- 距离
- 楼层
- 动动条
-
- 气温
- 最高 / 最低气温
- 日出
- 日落
- 卡路里
- 步数
- 距离
- 楼层
- 电量
- 心率
-
周日,周一,周二,周三,周四,周五,周六
1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月
$1$$2$日
上午,下午
- 气温,卡路里,步数,距离,楼层,电量,日出,日落,高低温,心率
-
- 温度单位
- 跟随手表
- 摄氏度 (°C)
- 华氏度 (°F)
diff --git a/resources-icons-240/drawables/drawables.xml b/resources-icons-240/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-240/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-240/drawables/sunrise.png b/resources-icons-240/drawables/sunrise.png
new file mode 100644
index 0000000..3184f14
Binary files /dev/null and b/resources-icons-240/drawables/sunrise.png differ
diff --git a/resources-icons-240/drawables/sunset.png b/resources-icons-240/drawables/sunset.png
new file mode 100644
index 0000000..e60eb2b
Binary files /dev/null and b/resources-icons-240/drawables/sunset.png differ
diff --git a/resources-icons-240/drawables/weather.png b/resources-icons-240/drawables/weather.png
new file mode 100644
index 0000000..dfb6b84
Binary files /dev/null and b/resources-icons-240/drawables/weather.png differ
diff --git a/resources-icons-260/drawables/drawables.xml b/resources-icons-260/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-260/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-260/drawables/sunrise.png b/resources-icons-260/drawables/sunrise.png
new file mode 100644
index 0000000..e901d32
Binary files /dev/null and b/resources-icons-260/drawables/sunrise.png differ
diff --git a/resources-icons-260/drawables/sunset.png b/resources-icons-260/drawables/sunset.png
new file mode 100644
index 0000000..6af60c4
Binary files /dev/null and b/resources-icons-260/drawables/sunset.png differ
diff --git a/resources-icons-260/drawables/weather.png b/resources-icons-260/drawables/weather.png
new file mode 100644
index 0000000..6c5e42a
Binary files /dev/null and b/resources-icons-260/drawables/weather.png differ
diff --git a/resources-icons-280/drawables/drawables.xml b/resources-icons-280/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-280/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-280/drawables/sunrise.png b/resources-icons-280/drawables/sunrise.png
new file mode 100644
index 0000000..9d5fec6
Binary files /dev/null and b/resources-icons-280/drawables/sunrise.png differ
diff --git a/resources-icons-280/drawables/sunset.png b/resources-icons-280/drawables/sunset.png
new file mode 100644
index 0000000..ddad99d
Binary files /dev/null and b/resources-icons-280/drawables/sunset.png differ
diff --git a/resources-icons-280/drawables/weather.png b/resources-icons-280/drawables/weather.png
new file mode 100644
index 0000000..fa6e229
Binary files /dev/null and b/resources-icons-280/drawables/weather.png differ
diff --git a/resources-icons-390/drawables/drawables.xml b/resources-icons-390/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-390/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-390/drawables/sunrise.png b/resources-icons-390/drawables/sunrise.png
new file mode 100644
index 0000000..67f053e
Binary files /dev/null and b/resources-icons-390/drawables/sunrise.png differ
diff --git a/resources-icons-390/drawables/sunset.png b/resources-icons-390/drawables/sunset.png
new file mode 100644
index 0000000..c755128
Binary files /dev/null and b/resources-icons-390/drawables/sunset.png differ
diff --git a/resources-icons-390/drawables/weather.png b/resources-icons-390/drawables/weather.png
new file mode 100644
index 0000000..45bddb9
Binary files /dev/null and b/resources-icons-390/drawables/weather.png differ
diff --git a/resources-icons-416/drawables/drawables.xml b/resources-icons-416/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-416/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-416/drawables/sunrise.png b/resources-icons-416/drawables/sunrise.png
new file mode 100644
index 0000000..10d3e22
Binary files /dev/null and b/resources-icons-416/drawables/sunrise.png differ
diff --git a/resources-icons-416/drawables/sunset.png b/resources-icons-416/drawables/sunset.png
new file mode 100644
index 0000000..9a6648d
Binary files /dev/null and b/resources-icons-416/drawables/sunset.png differ
diff --git a/resources-icons-416/drawables/weather.png b/resources-icons-416/drawables/weather.png
new file mode 100644
index 0000000..386fb2f
Binary files /dev/null and b/resources-icons-416/drawables/weather.png differ
diff --git a/resources-icons-454/drawables/drawables.xml b/resources-icons-454/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-454/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-454/drawables/sunrise.png b/resources-icons-454/drawables/sunrise.png
new file mode 100644
index 0000000..ba3a9a8
Binary files /dev/null and b/resources-icons-454/drawables/sunrise.png differ
diff --git a/resources-icons-454/drawables/sunset.png b/resources-icons-454/drawables/sunset.png
new file mode 100644
index 0000000..c772780
Binary files /dev/null and b/resources-icons-454/drawables/sunset.png differ
diff --git a/resources-icons-454/drawables/weather.png b/resources-icons-454/drawables/weather.png
new file mode 100644
index 0000000..1fb453e
Binary files /dev/null and b/resources-icons-454/drawables/weather.png differ
diff --git a/resources/drawables/drawables.xml b/resources-launcher-40/drawables/drawables.xml
similarity index 100%
rename from resources/drawables/drawables.xml
rename to resources-launcher-40/drawables/drawables.xml
diff --git a/resources-launcher-40/drawables/launcher_icon.png b/resources-launcher-40/drawables/launcher_icon.png
new file mode 100644
index 0000000..9b00253
Binary files /dev/null and b/resources-launcher-40/drawables/launcher_icon.png differ
diff --git a/resources-launcher-56/drawables/drawables.xml b/resources-launcher-56/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-56/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-56/drawables/launcher_icon.png b/resources-launcher-56/drawables/launcher_icon.png
new file mode 100644
index 0000000..71f9b18
Binary files /dev/null and b/resources-launcher-56/drawables/launcher_icon.png differ
diff --git a/resources-launcher-60/drawables/drawables.xml b/resources-launcher-60/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-60/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-60/drawables/launcher_icon.png b/resources-launcher-60/drawables/launcher_icon.png
new file mode 100644
index 0000000..b3f8583
Binary files /dev/null and b/resources-launcher-60/drawables/launcher_icon.png differ
diff --git a/resources-launcher-65/drawables/drawables.xml b/resources-launcher-65/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-65/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-65/drawables/launcher_icon.png b/resources-launcher-65/drawables/launcher_icon.png
new file mode 100644
index 0000000..b8bbe02
Binary files /dev/null and b/resources-launcher-65/drawables/launcher_icon.png differ
diff --git a/resources-launcher-70/drawables/drawables.xml b/resources-launcher-70/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-70/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-70/drawables/launcher_icon.png b/resources-launcher-70/drawables/launcher_icon.png
new file mode 100644
index 0000000..15ca2ff
Binary files /dev/null and b/resources-launcher-70/drawables/launcher_icon.png differ
diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png
deleted file mode 100644
index c257fff..0000000
Binary files a/resources/drawables/launcher_icon.png and /dev/null differ
diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml
index a85eaab..20ed93d 100644
--- a/resources/settings/properties.xml
+++ b/resources/settings/properties.xml
@@ -4,11 +4,18 @@
1
4
3
- 6
- 8
+
+ 712
+ 639
3
- 2
+ 1
0
+ 0
+ 0
+
+
+
true
true
true
diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml
index 6982840..464ed79 100644
--- a/resources/settings/settings.xml
+++ b/resources/settings/settings.xml
@@ -1,4 +1,5 @@
+
@Strings.ThemeEmber
@@ -11,6 +12,326 @@
+
+
+ @Strings.FldOff
+ @Strings.FldTime
+ @Strings.FldUtcTime
+ @Strings.FldAltTime1
+ @Strings.FldAltTime2
+ @Strings.FldDayMonthDay
+ @Strings.FldDayDate
+ @Strings.FldMonthDay
+ @Strings.FldDayOfMonth
+ @Strings.FldDayShort
+ @Strings.FldDayFull
+ @Strings.FldMonthName
+ @Strings.FldMonthNum
+ @Strings.FldMeridiem
+ @Strings.FldWeekIso
+ @Strings.FldWeekCal
+ @Strings.FldSteps
+ @Strings.FldStepsRound
+ @Strings.FldStepsToGo
+ @Strings.FldCalories
+ @Strings.FldActiveCal
+ @Strings.FldActiveMin
+ @Strings.FldActiveMod
+ @Strings.FldActiveVig
+ @Strings.FldActiveWeek
+ @Strings.FldDistance
+ @Strings.FldFloorsUp
+ @Strings.FldFloorsDown
+ @Strings.FldMetersUp
+ @Strings.FldPushDist
+ @Strings.FldPushes
+ @Strings.FldHeartRate
+ @Strings.FldRestingHr
+ @Strings.FldStress
+ @Strings.FldPulseOx
+ @Strings.FldRespiration
+ @Strings.FldBodyBattery
+ @Strings.FldMoveBar
+ @Strings.FldMoveBarRev
+ @Strings.FldWeight
+ @Strings.FldBmi
+ @Strings.FldRecovery
+ @Strings.FldVo2Run
+ @Strings.FldVo2Bike
+ @Strings.FldBattery
+ @Strings.FldBatteryDays
+ @Strings.FldBatteryBoth
+ @Strings.FldAlarms
+ @Strings.FldNotifications
+ @Strings.FldDoNotDisturb
+ @Strings.FldBluetooth
+ @Strings.FldSolar
+ @Strings.FldAltitude
+ @Strings.FldSeaPressure
+ @Strings.FldAmbPressure
+ @Strings.FldSunrise
+ @Strings.FldSunset
+ @Strings.FldNextSun
+ @Strings.FldUntilSun
+ @Strings.FldMoonAge
+ @Strings.FldMoonLit
+ @Strings.FldTemp
+ @Strings.FldTempHiLo
+ @Strings.FldFeelsLike
+ @Strings.FldTempHigh
+ @Strings.FldTempLow
+ @Strings.FldHumidity
+ @Strings.FldPrecip
+ @Strings.FldWindSpeed
+ @Strings.FldWindBearing
+ @Strings.FldWindDir
+ @Strings.FldWeatherTime
+ @Strings.FldCustom1
+ @Strings.FldCustom2
+ @Strings.FldCustom3
+
+
+
+
+
+ @Strings.FldOff
+ @Strings.FldTime
+ @Strings.FldUtcTime
+ @Strings.FldAltTime1
+ @Strings.FldAltTime2
+ @Strings.FldDayMonthDay
+ @Strings.FldDayDate
+ @Strings.FldMonthDay
+ @Strings.FldDayOfMonth
+ @Strings.FldDayShort
+ @Strings.FldDayFull
+ @Strings.FldMonthName
+ @Strings.FldMonthNum
+ @Strings.FldMeridiem
+ @Strings.FldWeekIso
+ @Strings.FldWeekCal
+ @Strings.FldSteps
+ @Strings.FldStepsRound
+ @Strings.FldStepsToGo
+ @Strings.FldCalories
+ @Strings.FldActiveCal
+ @Strings.FldActiveMin
+ @Strings.FldActiveMod
+ @Strings.FldActiveVig
+ @Strings.FldActiveWeek
+ @Strings.FldDistance
+ @Strings.FldFloorsUp
+ @Strings.FldFloorsDown
+ @Strings.FldMetersUp
+ @Strings.FldPushDist
+ @Strings.FldPushes
+ @Strings.FldHeartRate
+ @Strings.FldRestingHr
+ @Strings.FldStress
+ @Strings.FldPulseOx
+ @Strings.FldRespiration
+ @Strings.FldBodyBattery
+ @Strings.FldMoveBar
+ @Strings.FldMoveBarRev
+ @Strings.FldWeight
+ @Strings.FldBmi
+ @Strings.FldRecovery
+ @Strings.FldVo2Run
+ @Strings.FldVo2Bike
+ @Strings.FldBattery
+ @Strings.FldBatteryDays
+ @Strings.FldBatteryBoth
+ @Strings.FldAlarms
+ @Strings.FldNotifications
+ @Strings.FldDoNotDisturb
+ @Strings.FldBluetooth
+ @Strings.FldSolar
+ @Strings.FldAltitude
+ @Strings.FldSeaPressure
+ @Strings.FldAmbPressure
+ @Strings.FldSunrise
+ @Strings.FldSunset
+ @Strings.FldNextSun
+ @Strings.FldUntilSun
+ @Strings.FldMoonAge
+ @Strings.FldMoonLit
+ @Strings.FldTemp
+ @Strings.FldTempHiLo
+ @Strings.FldFeelsLike
+ @Strings.FldTempHigh
+ @Strings.FldTempLow
+ @Strings.FldHumidity
+ @Strings.FldPrecip
+ @Strings.FldWindSpeed
+ @Strings.FldWindBearing
+ @Strings.FldWindDir
+ @Strings.FldWeatherTime
+ @Strings.FldCustom1
+ @Strings.FldCustom2
+ @Strings.FldCustom3
+
+
+
+
+
+ @Strings.FldOff
+ @Strings.FldTime
+ @Strings.FldUtcTime
+ @Strings.FldAltTime1
+ @Strings.FldAltTime2
+ @Strings.FldDayMonthDay
+ @Strings.FldDayDate
+ @Strings.FldMonthDay
+ @Strings.FldDayOfMonth
+ @Strings.FldDayShort
+ @Strings.FldDayFull
+ @Strings.FldMonthName
+ @Strings.FldMonthNum
+ @Strings.FldMeridiem
+ @Strings.FldWeekIso
+ @Strings.FldWeekCal
+ @Strings.FldSteps
+ @Strings.FldStepsRound
+ @Strings.FldStepsToGo
+ @Strings.FldCalories
+ @Strings.FldActiveCal
+ @Strings.FldActiveMin
+ @Strings.FldActiveMod
+ @Strings.FldActiveVig
+ @Strings.FldActiveWeek
+ @Strings.FldDistance
+ @Strings.FldFloorsUp
+ @Strings.FldFloorsDown
+ @Strings.FldMetersUp
+ @Strings.FldPushDist
+ @Strings.FldPushes
+ @Strings.FldHeartRate
+ @Strings.FldRestingHr
+ @Strings.FldStress
+ @Strings.FldPulseOx
+ @Strings.FldRespiration
+ @Strings.FldBodyBattery
+ @Strings.FldMoveBar
+ @Strings.FldMoveBarRev
+ @Strings.FldWeight
+ @Strings.FldBmi
+ @Strings.FldRecovery
+ @Strings.FldVo2Run
+ @Strings.FldVo2Bike
+ @Strings.FldBattery
+ @Strings.FldBatteryDays
+ @Strings.FldBatteryBoth
+ @Strings.FldAlarms
+ @Strings.FldNotifications
+ @Strings.FldDoNotDisturb
+ @Strings.FldBluetooth
+ @Strings.FldSolar
+ @Strings.FldAltitude
+ @Strings.FldSeaPressure
+ @Strings.FldAmbPressure
+ @Strings.FldSunrise
+ @Strings.FldSunset
+ @Strings.FldNextSun
+ @Strings.FldUntilSun
+ @Strings.FldMoonAge
+ @Strings.FldMoonLit
+ @Strings.FldTemp
+ @Strings.FldTempHiLo
+ @Strings.FldFeelsLike
+ @Strings.FldTempHigh
+ @Strings.FldTempLow
+ @Strings.FldHumidity
+ @Strings.FldPrecip
+ @Strings.FldWindSpeed
+ @Strings.FldWindBearing
+ @Strings.FldWindDir
+ @Strings.FldWeatherTime
+ @Strings.FldCustom1
+ @Strings.FldCustom2
+ @Strings.FldCustom3
+
+
+
+
+
+ @Strings.FldOff
+ @Strings.FldTime
+ @Strings.FldUtcTime
+ @Strings.FldAltTime1
+ @Strings.FldAltTime2
+ @Strings.FldDayMonthDay
+ @Strings.FldDayDate
+ @Strings.FldMonthDay
+ @Strings.FldDayOfMonth
+ @Strings.FldDayShort
+ @Strings.FldDayFull
+ @Strings.FldMonthName
+ @Strings.FldMonthNum
+ @Strings.FldMeridiem
+ @Strings.FldWeekIso
+ @Strings.FldWeekCal
+ @Strings.FldSteps
+ @Strings.FldStepsRound
+ @Strings.FldStepsToGo
+ @Strings.FldCalories
+ @Strings.FldActiveCal
+ @Strings.FldActiveMin
+ @Strings.FldActiveMod
+ @Strings.FldActiveVig
+ @Strings.FldActiveWeek
+ @Strings.FldDistance
+ @Strings.FldFloorsUp
+ @Strings.FldFloorsDown
+ @Strings.FldMetersUp
+ @Strings.FldPushDist
+ @Strings.FldPushes
+ @Strings.FldHeartRate
+ @Strings.FldRestingHr
+ @Strings.FldStress
+ @Strings.FldPulseOx
+ @Strings.FldRespiration
+ @Strings.FldBodyBattery
+ @Strings.FldMoveBar
+ @Strings.FldMoveBarRev
+ @Strings.FldWeight
+ @Strings.FldBmi
+ @Strings.FldRecovery
+ @Strings.FldVo2Run
+ @Strings.FldVo2Bike
+ @Strings.FldBattery
+ @Strings.FldBatteryDays
+ @Strings.FldBatteryBoth
+ @Strings.FldAlarms
+ @Strings.FldNotifications
+ @Strings.FldDoNotDisturb
+ @Strings.FldBluetooth
+ @Strings.FldSolar
+ @Strings.FldAltitude
+ @Strings.FldSeaPressure
+ @Strings.FldAmbPressure
+ @Strings.FldSunrise
+ @Strings.FldSunset
+ @Strings.FldNextSun
+ @Strings.FldUntilSun
+ @Strings.FldMoonAge
+ @Strings.FldMoonLit
+ @Strings.FldTemp
+ @Strings.FldTempHiLo
+ @Strings.FldFeelsLike
+ @Strings.FldTempHigh
+ @Strings.FldTempLow
+ @Strings.FldHumidity
+ @Strings.FldPrecip
+ @Strings.FldWindSpeed
+ @Strings.FldWindBearing
+ @Strings.FldWindDir
+ @Strings.FldWeatherTime
+ @Strings.FldCustom1
+ @Strings.FldCustom2
+ @Strings.FldCustom3
+
+
+
@Strings.MetricSteps
@@ -18,6 +339,8 @@
@Strings.MetricDistance
@Strings.MetricFloors
@Strings.MetricMoveBar
+ @Strings.MetricActiveMin
+ @Strings.MetricBodyBattery
@@ -28,6 +351,8 @@
@Strings.MetricDistance
@Strings.MetricFloors
@Strings.MetricMoveBar
+ @Strings.MetricActiveMin
+ @Strings.MetricBodyBattery
@@ -38,6 +363,8 @@
@Strings.MetricDistance
@Strings.MetricFloors
@Strings.MetricMoveBar
+ @Strings.MetricActiveMin
+ @Strings.MetricBodyBattery
@@ -48,68 +375,11 @@
@Strings.MetricDistance
@Strings.MetricFloors
@Strings.MetricMoveBar
+ @Strings.MetricActiveMin
+ @Strings.MetricBodyBattery
-
-
- @Strings.FieldWeather
- @Strings.FieldHiLo
- @Strings.FieldSunrise
- @Strings.FieldSunset
- @Strings.FieldCalories
- @Strings.FieldSteps
- @Strings.FieldDistance
- @Strings.FieldFloors
- @Strings.FieldBattery
- @Strings.FieldHeartRate
-
-
-
-
-
- @Strings.FieldWeather
- @Strings.FieldHiLo
- @Strings.FieldSunrise
- @Strings.FieldSunset
- @Strings.FieldCalories
- @Strings.FieldSteps
- @Strings.FieldDistance
- @Strings.FieldFloors
- @Strings.FieldBattery
- @Strings.FieldHeartRate
-
-
-
-
-
- @Strings.FieldWeather
- @Strings.FieldHiLo
- @Strings.FieldSunrise
- @Strings.FieldSunset
- @Strings.FieldCalories
- @Strings.FieldSteps
- @Strings.FieldDistance
- @Strings.FieldFloors
- @Strings.FieldBattery
- @Strings.FieldHeartRate
-
-
-
-
-
- @Strings.FieldWeather
- @Strings.FieldHiLo
- @Strings.FieldSunrise
- @Strings.FieldSunset
- @Strings.FieldCalories
- @Strings.FieldSteps
- @Strings.FieldDistance
- @Strings.FieldFloors
- @Strings.FieldBattery
- @Strings.FieldHeartRate
-
-
@@ -119,6 +389,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/strings/fields.xml b/resources/strings/fields.xml
new file mode 100644
index 0000000..99e8030
--- /dev/null
+++ b/resources/strings/fields.xml
@@ -0,0 +1,89 @@
+
+
+ Off
+ Time
+ UTC Time
+ Alternate Time 1
+ Alternate Time 2
+ Day Month Day
+ Day Date
+ Month Day
+ Day of Month
+ Day (Tue)
+ Full Day (Tuesday)
+ Month (Jan)
+ Month (6)
+ AM/PM
+ Week Number (ISO)
+ Week Number
+ Steps
+ Steps Rounded
+ Steps to Go
+ Calories
+ Active Calories
+ Active Minutes
+ Active Min (Moderate)
+ Active Min (Vigorous)
+ Active Minutes (Week)
+ Distance
+ Floors Climbed
+ Floors Descended
+ Distance Climbed
+ Push Distance
+ Pushes
+ Heart Rate
+ Resting Heart Rate
+ Stress Score
+ Pulse Ox
+ Respiration Rate
+ Body Battery
+ Move Bar
+ Move Bar Reversed
+ Weight
+ BMI
+ Time to Recovery
+ VO2 Max (Run)
+ VO2 Max (Bike)
+ Battery
+ Battery in Days
+ Battery / Days
+ Alarms
+ Notifications
+ Do Not Disturb
+ Bluetooth
+ Solar Intensity
+ Altitude
+ Sea Level Pressure
+ Ambient Pressure
+ Sunrise
+ Sunset
+ Next Sun Event
+ Time Until Sun Event
+ Moon Age
+ Moon Illumination
+ Temperature
+ Temperature High/Low
+ Feels Like
+ High Temperature
+ Low Temperature
+ Humidity
+ Precipitation %
+ Wind Speed
+ Wind Bearing
+ Wind Direction
+ Weather Update Time
+ Custom Text 1
+ Custom Text 2
+ Custom Text 3
+
+
+ ,TIME,UTC,TZ1,TZ2,DATE,DATE,DATE,DAY,DAY,DAY,MON,MON,,WEEK,WEEK,STEP,STEP,TO GO,KCAL,ACAL,MIN,MOD,VIG,WEEK,DST,FLR,DESC,ASC,PUSH,PUSH,HR,RHR,STRS,SPO2,RESP,BODY,MOVE,MOVE,WT,BMI,RCVR,VO2,VO2,BATT,DAYS,BATT,ALRM,MSG,DND,BT,SOLR,ALT,hPa,hPa,RISE,SET,SUN,SUN,MOON,MOON,TEMP,HI/LO,FEEL,HIGH,LOW,HUM,RAIN,WIND,WIND,WIND,WX,,,
+
+ Steps
+ Calories
+ Distance
+ Floors
+ Move Bar
+ Active Minutes
+ Body Battery
+
diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml
index ae399b9..babfaf1 100644
--- a/resources/strings/strings.xml
+++ b/resources/strings/strings.xml
@@ -6,14 +6,24 @@
Ring · Top-Left
Ring · Bottom-Right
Ring · Bottom-Left
- Top-Left Complication
- Top-Right Complication
+ Top-Left Field
+ Top-Right Field
Bottom-Left Field
Bottom-Right Field
+ Temperature Unit
+ Alternate Time 1 (minutes from local)
+ Alternate Time 2 (minutes from local)
+ Custom Text 1
+ Custom Text 2
+ Custom Text 3
Show Top Row
Show Date Band
Show Bottom Row
+ Follow watch
+ Celsius (°C)
+ Fahrenheit (°F)
+
1 · Ember
2 · Aurora
3 · Brass
@@ -22,32 +32,9 @@
6 · Acid
7 · Kelp
- Steps
- Calories
- Distance
- Floors
- Move Bar
-
- Temperature
- Hi / Lo Temp
- Sunrise
- Sunset
- Calories
- Steps
- Distance
- Floors
- Battery
- Heart Rate
-
SUN,MON,TUE,WED,THU,FRI,SAT
JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
$1$ $2$
AM,PM
- TEMP,KCAL,STEP,DST,FLR,BATT,RISE,SET,HI/LO,HR
-
- Temperature Unit
- Follow watch
- Celsius (°C)
- Fahrenheit (°F)
diff --git a/source/Fenix8V3View.mc b/source/Fenix8V3View.mc
index be4633d..a2d6500 100644
--- a/source/Fenix8V3View.mc
+++ b/source/Fenix8V3View.mc
@@ -11,6 +11,8 @@ import Toybox.Time;
import Toybox.Time.Gregorian;
import Toybox.WatchUi;
import Themes;
+import Fields;
+import Toybox.SensorHistory;
// Every literal below is a coordinate on the 500x500 design canvas, measured
// from design/watchface-*.svg. Nothing here is eyeballed: ring geometry comes
@@ -47,7 +49,7 @@ class Fenix8V3View extends WatchUi.WatchFace {
const LEFT_X = 157.00; // top complications, centred
const RIGHT_X = 343.70;
- const ICON_CY = 90.00;
+ const ICON_CY = 89.50;
const VALUE_CY = 131.06;
const TIME_CY = 221.10;
@@ -89,22 +91,37 @@ class Fenix8V3View extends WatchUi.WatchFace {
private var mFontBand = Graphics.FONT_SMALL;
private var mFontComp = Graphics.FONT_MEDIUM;
private var mFontTime = Graphics.FONT_NUMBER_HOT;
- private var mDow = null;
- private var mMon = null;
- private var mLabels = null;
- private var mMeridiem = null;
- private var mDateFmt = " ";
+ // Always-on display: AMOLED devices must cut lit pixels hard while
+ // asleep, and shift what is left so nothing burns in.
+ private var mSleeping = false;
+ private var mBurnIn = false;
+ private var mIconSunrise = null;
+ private var mIconSunset = null;
+ private var mIconWeather = null;
function initialize() {
WatchFace.initialize();
}
+ function onEnterSleep() as Void {
+ mSleeping = true;
+ WatchUi.requestUpdate();
+ }
+
+ function onExitSleep() as Void {
+ mSleeping = false;
+ WatchUi.requestUpdate();
+ }
+
function onLayout(dc as Dc) as Void {
mW = dc.getWidth();
mH = dc.getHeight();
mS = mW / 500.0;
mCx = mW / 2;
mCy = mH / 2;
+ var ds = System.getDeviceSettings();
+ mBurnIn = (ds != null) && (ds has :requiresBurnInProtection)
+ && ds.requiresBurnInProtection;
var text = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL,
Graphics.FONT_MEDIUM, Graphics.FONT_LARGE];
var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM,
@@ -115,28 +132,11 @@ class Fenix8V3View extends WatchUi.WatchFace {
loadStrings();
}
- // Every piece of on-face wording lives in resources so resources-chn can
- // replace it. loadResource is not cheap, so pull it all once per layout.
function loadStrings() as Void {
- mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
- mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
- mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
- mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
- mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
- }
-
- // Monkey C has no String.split.
- function splitCsv(str as String) as Array {
- var out = [] as Array;
- var rest = str;
- var cut = rest.find(",");
- while (cut != null) {
- out.add(rest.substring(0, cut) as String);
- rest = rest.substring(cut + 1, rest.length()) as String;
- cut = rest.find(",");
- }
- out.add(rest);
- return out;
+ Fields.loadStrings();
+ mIconSunrise = WatchUi.loadResource(Rez.Drawables.IconSunrise);
+ mIconSunset = WatchUi.loadResource(Rez.Drawables.IconSunset);
+ mIconWeather = WatchUi.loadResource(Rez.Drawables.IconWeather);
}
// Connect IQ exposes ascent and descent but not cap height, and the ascent
@@ -254,6 +254,15 @@ class Fenix8V3View extends WatchUi.WatchFace {
? info.floorsClimbedGoal.toFloat() : 10.0f;
p = info.floorsClimbed.toFloat() / goal;
}
+ } else if (idx == 5) { // Active minutes vs the weekly goal
+ if (info.activeMinutesWeek != null && info.activeMinutesWeekGoal != null
+ && info.activeMinutesWeekGoal > 0) {
+ p = info.activeMinutesWeek.total.toFloat()
+ / info.activeMinutesWeekGoal.toFloat();
+ }
+ } else if (idx == 6) { // Body battery is already 0..100
+ var bb = Fields.latest(SensorHistory.getBodyBatteryHistory({:period => 1}));
+ if (bb != null) { p = bb.toFloat() / 100.0; }
} else if (idx == 4) { // Move bar: an integer level, not a ratio
if (info.moveBarLevel != null) {
var span = (ActivityMonitor.MOVE_BAR_LEVEL_MAX
@@ -268,103 +277,6 @@ class Fenix8V3View extends WatchUi.WatchFace {
return p;
}
- // ------------------------------------------------------------ data bits
- function distanceString(info as ActivityMonitor.Info, ds as System.DeviceSettings) as String {
- if (info == null || info.distance == null) { return "0.0"; }
- var km = info.distance.toFloat() / 100000.0;
- if (ds != null && ds.distanceUnits == System.UNIT_STATUTE) { km = km * 0.621371; }
- return km.format("%.1f");
- }
-
- // TempUnit: 0 follow the watch, 1 force Celsius, 2 force Fahrenheit.
- function tempString(c as Numeric?, ds as System.DeviceSettings) as String {
- if (c == null) { return "--"; }
- var t = c.toFloat();
- var mode = getSetting("TempUnit", 0);
- var fahrenheit = (mode == 2)
- || (mode == 0 && ds != null && ds.temperatureUnits == System.UNIT_STATUTE);
- if (fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
- return t.format("%d") + "°";
- }
-
- function clockString(m as Time.Moment?, ds as System.DeviceSettings) as String {
- if (m == null) { return "--:--"; }
- var g = Gregorian.info(m, Time.FORMAT_SHORT);
- var h = g.hour;
- if (ds == null || !ds.is24Hour) {
- if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
- }
- return h.format("%d") + ":" + g.min.format("%02d");
- }
-
- function here(weather as Weather.CurrentConditions?) as Position.Location? {
- if (weather != null && weather.observationLocationPosition != null) {
- return weather.observationLocationPosition;
- }
- var act = Activity.getActivityInfo();
- if (act != null && act.currentLocation != null) { return act.currentLocation; }
- return null;
- }
-
- // value string for a complication slot
- function complicationValue(typeIdx as Number, info as ActivityMonitor.Info,
- weather as Weather.CurrentConditions?, stats as System.Stats,
- ds as System.DeviceSettings) as String {
- if (typeIdx == 1) { // Calories
- return info != null && info.calories != null ? info.calories.format("%d") : "0";
- } else if (typeIdx == 2) { // Steps
- return info != null && info.steps != null ? info.steps.format("%d") : "0";
- } else if (typeIdx == 3) { // Distance
- return distanceString(info, ds);
- } else if (typeIdx == 4) { // Floors
- return info != null && info.floorsClimbed != null
- ? info.floorsClimbed.format("%d") : "0";
- } else if (typeIdx == 5) { // Battery
- var pct = 0;
- if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
- return pct.format("%d") + "%";
- } else if (typeIdx == 6 || typeIdx == 7) { // Sunrise / sunset
- var loc = here(weather);
- if (loc == null) { return "--:--"; }
- var m = typeIdx == 6 ? Weather.getSunrise(loc, Time.now())
- : Weather.getSunset(loc, Time.now());
- return clockString(m, ds);
- } else if (typeIdx == 9) { // Heart rate
- var act = Activity.getActivityInfo();
- if (act != null && act.currentHeartRate != null) {
- return act.currentHeartRate.format("%d");
- }
- var hist = ActivityMonitor.getHeartRateHistory(1, true);
- if (hist != null) {
- var sample = hist.next();
- if (sample != null && sample.heartRate != null
- && sample.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
- return sample.heartRate.format("%d");
- }
- }
- return "--";
- } else if (typeIdx == 8) { // Forecast high / low
- var fc = Weather.getDailyForecast();
- if (fc != null && fc.size() > 0) {
- return tempString(fc[0].highTemperature, ds) + "/"
- + tempString(fc[0].lowTemperature, ds);
- }
- return "--/--";
- }
- // 0: current temperature
- if (weather != null && weather.temperature != null) {
- return tempString(weather.temperature, ds);
- }
- return "--";
- }
-
- // Short caption under a bottom-row value. Indexed like the field types, so
- // the label always follows whatever the slot is set to.
- function fieldLabel(typeIdx as Number) as String {
- if (mLabels == null || typeIdx < 0 || typeIdx >= mLabels.size()) { return ""; }
- return mLabels[typeIdx];
- }
-
// ----------------------------------------------------------------- ring
function tick(dc as Dc, ang as Float, hw as Float) as Void {
var a0 = ang - hw;
@@ -441,79 +353,19 @@ class Fenix8V3View extends WatchUi.WatchFace {
}
}
- // Filled half-ellipse, flat side down. The comp's dome is 14.90 wide but
- // 8.93 tall, so it is not a plain semicircle.
- function dome(dc as Dc, cx as Numeric, baseY as Numeric,
- rx as Numeric, ry as Numeric) as Void {
- var pts = [];
- for (var i = 0; i <= 12; i++) {
- var rad = Math.toRadians(180.0 - 15.0 * i);
- pts.add([Math.round(px(cx) + Math.cos(rad) * px(rx)).toNumber(),
- Math.round(px(baseY) - Math.sin(rad) * px(ry)).toNumber()]);
- }
- dc.fillPolygon(pts);
- }
-
function stub(dc as Dc, x1 as Numeric, y1 as Numeric, x2 as Numeric, y2 as Numeric) as Void {
dc.drawLine(px(x1), px(y1), px(x2), px(y2));
}
- // Sun over a horizon bar with a chevron. Every ray in the comp is a short
- // stub roughly as long as the pen is wide -- drawing them longer is what
- // made this icon read taller than the weather one.
- // dir = -1 flips the chevron up, which is what the sunset slot uses.
- function iconSun(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
- col as Number, dir as Number) as Void {
- var k = scale;
- var ox = cx - 156.32 * k; // design-space offset of this icon
- var oy = cy - 90.34 * k;
- dc.setColor(col, Graphics.COLOR_TRANSPARENT);
- dome(dc, ox + 156.39 * k, oy + 94.02 * k, 7.45 * k, 8.93 * k);
- dc.setPenWidth(maxNum(2, px(2.96 * k)));
- stub(dc, ox + 156.39 * k, oy + 79.4 * k, ox + 156.39 * k, oy + 81.1 * k);
- stub(dc, ox + 148.2 * k, oy + 83.3 * k, ox + 146.5 * k, oy + 84.5 * k);
- stub(dc, ox + 164.6 * k, oy + 84.5 * k, ox + 166.3 * k, oy + 83.3 * k);
- stub(dc, ox + 142.6 * k, oy + 92.5 * k, ox + 144.7 * k, oy + 92.5 * k);
- stub(dc, ox + 168.0 * k, oy + 92.5 * k, ox + 170.1 * k, oy + 92.5 * k);
- dc.setPenWidth(maxNum(2, px(2.6 * k)));
- var by = oy + 98.45 * k;
- stub(dc, ox + 142.3 * k, by, ox + 170.3 * k, by);
- stub(dc, ox + 152.6 * k, by, ox + 156.3 * k, by + dir * 3.05 * k);
- stub(dc, ox + 156.3 * k, by + dir * 3.05 * k, ox + 160.0 * k, by);
- }
-
- // Sun peeking out from behind a cloud. The comp keeps a dark notch where
- // the cloud crosses the sun; without it the two shapes merge into one blob
- // and the icon reads much heavier than the sunrise one next to it. So the
- // cloud is stamped twice: once inflated in black to cut the notch, then at
- // true size in the accent colour.
- function cloudBody(dc as Dc, ox as Numeric, oy as Numeric, k as Float,
- grow as Float) as Void {
- dc.fillCircle(px(ox + 336.6 * k), px(oy + 94.4 * k), px(8.7 * k + grow));
- dc.fillCircle(px(ox + 346.4 * k), px(oy + 96.9 * k), px(6.3 * k + grow));
- var g = px(grow);
- dc.fillRoundedRectangle(px(ox + 328.0 * k) - g, px(oy + 95.5 * k) - g,
- px(24.6 * k) + 2 * g, px(7.75 * k) + g,
- px(3.4 * k));
- }
-
- function iconCloud(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
- col as Number) as Void {
- var k = scale;
- var ox = cx - 343.55 * k;
- var oy = cy - 89.25 * k;
- dc.setColor(col, Graphics.COLOR_TRANSPARENT);
- dc.fillCircle(px(ox + 347.0 * k), px(oy + 88.9 * k), px(7.8 * k));
- dc.setPenWidth(maxNum(2, px(2.3 * k)));
- stub(dc, ox + 346.2 * k, oy + 76.0 * k, ox + 346.2 * k, oy + 78.7 * k);
- stub(dc, ox + 354.9 * k, oy + 87.7 * k, ox + 357.5 * k, oy + 87.7 * k);
- dc.setPenWidth(maxNum(2, px(2.7 * k)));
- stub(dc, ox + 339.5 * k, oy + 79.5 * k, ox + 338.1 * k, oy + 80.9 * k);
- stub(dc, ox + 353.5 * k, oy + 80.9 * k, ox + 354.6 * k, oy + 79.8 * k);
- dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
- cloudBody(dc, ox, oy, k, 1.7 * k);
- dc.setColor(col, Graphics.COLOR_TRANSPARENT);
- cloudBody(dc, ox, oy, k, 0.0);
+ // The comps' sunrise and weather icons are traced outlines with two- and
+ // three-pixel features. Rebuilding them from circles and lines held up at
+ // 454 px and turned to mush at 280, so they ship as bitmaps rasterised
+ // from the design paths at each screen size (tools/gen_icons.py) and get
+ // the theme's accent applied through :tintColor.
+ function blitIcon(dc as Dc, bmp, cx as Numeric, cy as Numeric, col as Number) as Void {
+ if (bmp == null) { return; }
+ dc.drawBitmap2(px(cx) - bmp.getWidth() / 2, px(cy) - bmp.getHeight() / 2,
+ bmp, {:tintColor => col});
}
function maxNum(a as Number, b as Number) as Number {
@@ -522,47 +374,52 @@ class Fenix8V3View extends WatchUi.WatchFace {
function drawComplicationIcon(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
col as Number, typeIdx as Number, level as Float) as Void {
- if (typeIdx == 6) {
- iconSun(dc, cx, cy, scale, col, 1);
- } else if (typeIdx == 7) {
- iconSun(dc, cx, cy, scale, col, -1);
- } else if (typeIdx == 0 || typeIdx == 8) {
- iconCloud(dc, cx, cy, scale, col);
- } else if (typeIdx == 5) {
+ if (typeIdx == 712 || typeIdx == 700) {
+ blitIcon(dc, mIconSunrise, cx, ICON_CY, col);
+ } else if (typeIdx == 717 || typeIdx == 731) {
+ blitIcon(dc, mIconSunset, cx, ICON_CY, col);
+ } else if (typeIdx == 306 || typeIdx == 639 || typeIdx == 300
+ || typeIdx == 301 || typeIdx == 302 || typeIdx == 304
+ || typeIdx == 305 || typeIdx == 308 || typeIdx == 309
+ || typeIdx == 310 || typeIdx == 307) {
+ blitIcon(dc, mIconWeather, cx, ICON_CY, col);
+ } else if (typeIdx == 250 || typeIdx == 251 || typeIdx == 257) {
iconBattery(dc, cx - 14.4 * scale, cy, scale, col, level);
} else {
var r = 13.0 * scale;
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.setPenWidth(maxNum(2, px(3.0 * scale)));
- if (typeIdx == 1) { // calories: flame
+ if (typeIdx == 2 || typeIdx == 603) { // calories: flame
dc.fillPolygon([[px(cx), px(cy - r)],
[px(cx + r * 0.72), px(cy + r * 0.25)],
[px(cx + r * 0.40), px(cy + r * 0.85)],
[px(cx - r * 0.40), px(cy + r * 0.85)],
[px(cx - r * 0.72), px(cy + r * 0.25)]]);
- } else if (typeIdx == 2) { // steps: two prints
+ } else if (typeIdx == 1 || typeIdx == 14 || typeIdx == 707) { // steps
dc.fillRoundedRectangle(px(cx - r * 0.85), px(cy - r * 0.75),
px(r * 0.62), px(r * 1.15), px(r * 0.3));
dc.fillRoundedRectangle(px(cx + r * 0.22), px(cy - r * 0.35),
px(r * 0.62), px(r * 1.15), px(r * 0.3));
- } else if (typeIdx == 3) { // distance: arrow
+ } else if (typeIdx == 3 || typeIdx == 9 || typeIdx == 615) { // distance
dc.fillPolygon([[px(cx), px(cy - r)],
[px(cx + r * 0.75), px(cy + r * 0.8)],
[px(cx), px(cy + r * 0.35)],
[px(cx - r * 0.75), px(cy + r * 0.8)]]);
- } else if (typeIdx == 9) { // heart rate
+ } else if (typeIdx == 618 || typeIdx == 276 || typeIdx == 623) { // heart
dc.fillCircle(px(cx - r * 0.38), px(cy - r * 0.28), px(r * 0.44));
dc.fillCircle(px(cx + r * 0.38), px(cy - r * 0.28), px(r * 0.44));
dc.fillPolygon([[px(cx - r * 0.78), px(cy - r * 0.12)],
[px(cx + r * 0.78), px(cy - r * 0.12)],
[px(cx), px(cy + r * 0.85)]]);
- } else { // floors: stairs
+ } else if (typeIdx == 4 || typeIdx == 5 || typeIdx == 6) { // stairs
for (var i = 0; i < 3; i++) {
dc.fillRectangle(px(cx - r * 0.85 + r * 0.58 * i),
px(cy + r * 0.6 - r * 0.55 * i),
px(r * 0.62), px(r * 0.4));
}
}
+ // Everything else -- dates, week numbers, custom text -- reads fine
+ // without a glyph, so no icon is drawn.
}
}
@@ -578,14 +435,10 @@ class Fenix8V3View extends WatchUi.WatchFace {
}
function drawTopComplication(dc as Dc, ti as Number, typeIdx as Number, isLeft as Boolean,
- info as ActivityMonitor.Info, weather as Weather.CurrentConditions?,
- stats as System.Stats, ds as System.DeviceSettings) as Void {
+ ctx as Fields.Ctx, level as Float) as Void {
var x = isLeft ? LEFT_X : RIGHT_X;
- var level = 0.0f;
- if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
drawComplicationIcon(dc, x, ICON_CY, 1.0, Themes.ACCENT[ti] as Number, typeIdx, level);
- text(dc, x, VALUE_CY, mFontComp,
- complicationValue(typeIdx, info, weather, stats, ds),
+ text(dc, x, VALUE_CY, mFontComp, Fields.value(typeIdx, ctx),
Graphics.TEXT_JUSTIFY_CENTER, Themes.TEXTPRIMARY[ti] as Number);
}
@@ -615,15 +468,12 @@ class Fenix8V3View extends WatchUi.WatchFace {
// FORMAT_SHORT, not FORMAT_MEDIUM: only the short form returns
// day_of_week and month as numbers rather than localised strings.
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
- text(dc, DOW_CX, BAND_CY, mFontBand, mDow[g.day_of_week - 1],
+ text(dc, DOW_CX, BAND_CY, mFontBand, Fields.dow(g),
Graphics.TEXT_JUSTIFY_CENTER, col);
- // DateFormat is "$1$ $2$" in English and "$1$$2$日" in Chinese, where
- // the Chinese month names already carry their own 月.
- text(dc, MD_CX, BAND_CY, mFontBand,
- Lang.format(mDateFmt, [mMon[g.month - 1], g.day.format("%d")]),
+ text(dc, MD_CX, BAND_CY, mFontBand, Fields.monthDay(g),
Graphics.TEXT_JUSTIFY_CENTER, col);
if (ds == null || !ds.is24Hour) {
- text(dc, AMPM_R, BAND_CY, mFontBand, mMeridiem[g.hour >= 12 ? 1 : 0],
+ text(dc, AMPM_R, BAND_CY, mFontBand, Fields.meridiem(g),
Graphics.TEXT_JUSTIFY_RIGHT, col);
}
}
@@ -653,49 +503,120 @@ class Fenix8V3View extends WatchUi.WatchFace {
dc.fillRectangle(px(COLON_X), px(COLON_Y2), px(COLON_W), px(COLON_H));
}
- function drawBottom(dc as Dc, ti as Number, info as ActivityMonitor.Info,
- weather as Weather.CurrentConditions?, stats as System.Stats,
- ds as System.DeviceSettings) as Void {
+ function drawBottom(dc as Dc, ti as Number, ctx as Fields.Ctx) as Void {
var value = Themes.TEXTPRIMARY[ti] as Number;
var label = Themes.ACCENT[ti] as Number;
var left = getSetting("BottomLeft", 3);
- var right = getSetting("BottomRight", 2);
- text(dc, DIST_R, BOT_VAL_CY, mFontComp,
- complicationValue(left, info, weather, stats, ds),
+ var right = getSetting("BottomRight", 1);
+ text(dc, DIST_R, BOT_VAL_CY, mFontComp, Fields.value(left, ctx),
Graphics.TEXT_JUSTIFY_RIGHT, value);
- text(dc, DIST_R, BOT_LAB_CY, mFontComp, fieldLabel(left),
+ text(dc, DIST_R, BOT_LAB_CY, mFontComp, Fields.label(left),
Graphics.TEXT_JUSTIFY_RIGHT, label);
- text(dc, STEP_L, BOT_VAL_CY, mFontComp,
- complicationValue(right, info, weather, stats, ds),
+ text(dc, STEP_L, BOT_VAL_CY, mFontComp, Fields.value(right, ctx),
Graphics.TEXT_JUSTIFY_LEFT, value);
- text(dc, STEP_L, BOT_LAB_CY, mFontComp, fieldLabel(right),
+ text(dc, STEP_L, BOT_LAB_CY, mFontComp, Fields.label(right),
Graphics.TEXT_JUSTIFY_LEFT, label);
}
+ // ------------------------------------------------------------------ AOD
+ function dim(col as Number, f as Float) as Number {
+ var r = (((col >> 16) & 0xFF) * f).toNumber();
+ var g = (((col >> 8) & 0xFF) * f).toNumber();
+ var b = ((col & 0xFF) * f).toNumber();
+ return (r << 16) | (g << 8) | b;
+ }
+
+ // Low-power face: no band fill, no unlit ticks, no complications -- just a
+ // dimmed time, the lit part of the ring, and the date. The whole thing
+ // walks a few pixels each minute so no pixel stays lit in one place.
+ function drawAmbient(dc as Dc, ti as Number, ds as System.DeviceSettings,
+ pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
+ var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
+ var ox = ((g.min % 5) - 2) * 2.0 / mS; // +/- 4 device px, in design units
+ var oy = ((g.min / 5) % 5 - 2) * 2.0 / mS;
+
+ var top = Themes.RINGTOP[ti] as Array;
+ var bot = Themes.RINGBOTTOM[ti] as Array;
+ var litTR = (pTR * TOP_N).toNumber();
+ var litTL = (pTL * TOP_N).toNumber();
+ var litBR = (pBR * BOT_N).toNumber();
+ var litBL = (pBL * BOT_N).toNumber();
+ for (var i = 0; i < TOP_N; i++) {
+ var a = TOP_PH + TOP_PITCH * i;
+ if (i >= TOP_N - litTR) {
+ dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
+ tick(dc, a, TOP_HW);
+ }
+ if (i >= TOP_N - litTL) {
+ dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
+ tick(dc, -a, TOP_HW);
+ }
+ }
+ for (var i = 0; i < BOT_N; i++) {
+ var a = BOT_PH + BOT_PITCH * i;
+ if (i < litBL) {
+ dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
+ tick(dc, 180.0 + a, BOT_HW);
+ }
+ if (i < litBR) {
+ dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
+ tick(dc, 180.0 - a, BOT_HW);
+ }
+ }
+
+ var h = g.hour;
+ if (ds == null || !ds.is24Hour) {
+ if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
+ }
+ var hours = Themes.HOURS[ti] as Array;
+ var mins = Themes.MINUTES[ti] as Array;
+ text(dc, HOURS_R + ox, TIME_CY + oy, mFontTime, h.format("%d"),
+ Graphics.TEXT_JUSTIFY_RIGHT, dim(hours[hours.size() / 2], 0.55));
+ text(dc, MINUTES_L + ox, TIME_CY + oy, mFontTime, g.min.format("%02d"),
+ Graphics.TEXT_JUSTIFY_LEFT, dim(mins[mins.size() / 2], 0.55));
+ dc.setColor(dim(Themes.COLON[ti] as Number, 0.55), Graphics.COLOR_TRANSPARENT);
+ dc.fillRectangle(px(COLON_X + ox), px(COLON_Y1 + oy), px(COLON_W), px(COLON_H));
+ dc.fillRectangle(px(COLON_X + ox), px(COLON_Y2 + oy), px(COLON_W), px(COLON_H));
+
+ var band = dim(Themes.ACCENT[ti] as Number, 0.55);
+ text(dc, DOW_CX + ox, BAND_CY + oy, mFontBand, Fields.dow(g),
+ Graphics.TEXT_JUSTIFY_CENTER, band);
+ text(dc, MD_CX + ox, BAND_CY + oy, mFontBand, Fields.monthDay(g),
+ Graphics.TEXT_JUSTIFY_CENTER, band);
+ }
+
// ----------------------------------------------------------------- main
function onUpdate(dc as Dc) as Void {
- if (mW != dc.getWidth() || mDow == null) { onLayout(dc); }
+ if (mW != dc.getWidth() || !Fields.ready()) { onLayout(dc); }
if (dc has :setAntiAlias) { dc.setAntiAlias(true); }
var ti = themeIndex();
- var info = ActivityMonitor.getInfo();
- var stats = System.getSystemStats();
var ds = System.getDeviceSettings();
- var weather = Weather.getCurrentConditions();
+ var ctx = new Fields.Ctx(ds, getSetting("TempUnit", 0));
+ var info = ctx.info();
+ var stats = ctx.stats();
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
dc.clear();
- drawRing(dc, ti,
- metricProgress(getSetting("RingTR", 0), info),
- metricProgress(getSetting("RingTL", 1), info),
- metricProgress(getSetting("RingBR", 4), info),
- metricProgress(getSetting("RingBL", 3), info));
+ var pTR = metricProgress(getSetting("RingTR", 0), info);
+ var pTL = metricProgress(getSetting("RingTL", 1), info);
+ var pBR = metricProgress(getSetting("RingBR", 4), info);
+ var pBL = metricProgress(getSetting("RingBL", 3), info);
+
+ if (mSleeping && mBurnIn) {
+ drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL);
+ return;
+ }
+
+ drawRing(dc, ti, pTR, pTL, pBR, pBL);
if (getBool("ShowTop", true)) {
+ var level = 0.0f;
+ if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
drawBattery(dc, ti, stats);
- drawTopComplication(dc, ti, getSetting("LeftTop", 6), true, info, weather, stats, ds);
- drawTopComplication(dc, ti, getSetting("RightTop", 8), false, info, weather, stats, ds);
+ drawTopComplication(dc, ti, getSetting("LeftTop", 712), true, ctx, level);
+ drawTopComplication(dc, ti, getSetting("RightTop", 639), false, ctx, level);
}
drawTime(dc, ti, ds);
@@ -705,7 +626,7 @@ class Fenix8V3View extends WatchUi.WatchFace {
}
if (getBool("ShowBottom", true)) {
- drawBottom(dc, ti, info, weather, stats, ds);
+ drawBottom(dc, ti, ctx);
}
}
}
diff --git a/source/FieldTable.mc b/source/FieldTable.mc
new file mode 100644
index 0000000..00b1cfb
--- /dev/null
+++ b/source/FieldTable.mc
@@ -0,0 +1,22 @@
+// AUTO-GENERATED by tools/gen_fields.py -- do not edit by hand.
+// Ids follow https://watchface.io/docs/datafields; only the fields a
+// Connect IQ watch face can source on-device are listed.
+import Toybox.Lang;
+
+module FieldTable {
+
+ // Field ids, in the same order as the Labels resource string.
+ const IDS = [
+ 0,860,880,864,865,851,872,852,855,870,856,853,868,858,859,873,1,14,707,2,603,150,
+ 152,154,151,3,4,5,6,9,10,618,276,12,635,11,623,8,15,281,742,13,278,277,250,251,257,
+ 253,255,254,256,252,615,616,733,712,717,700,731,706,732,306,639,300,301,302,305,304,
+ 308,307,310,309,703,704,705
+ ];
+
+ function indexOf(id as Number) as Number {
+ for (var i = 0; i < IDS.size(); i++) {
+ if (IDS[i] == id) { return i; }
+ }
+ return 0;
+ }
+}
diff --git a/source/Fields.mc b/source/Fields.mc
new file mode 100644
index 0000000..3cca859
--- /dev/null
+++ b/source/Fields.mc
@@ -0,0 +1,491 @@
+import Toybox.Lang;
+import Toybox.Math;
+import Toybox.System;
+import Toybox.Time;
+import Toybox.Time.Gregorian;
+import Toybox.Application;
+import Toybox.ActivityMonitor;
+import Toybox.Activity;
+import Toybox.UserProfile;
+import Toybox.SensorHistory;
+import Toybox.Weather;
+import Toybox.Position;
+import Toybox.WatchUi;
+import FieldTable;
+
+// Everything a complication slot can show. Ids match
+// https://watchface.io/docs/datafields so the two catalogues line up; see
+// tools/gen_fields.py for the table that drives the settings and the labels.
+//
+// Only fields Connect IQ can answer from the watch itself are here. The
+// week-to-date and 7/28-day per-sport aggregates in that catalogue come from a
+// phone-side Garmin Connect integration, and the OpenWeatherMap / StormGlass /
+// third-party-app fields need network calls and API keys; neither is something
+// a self-contained watch face can produce.
+module Fields {
+
+ const DASH = "--";
+
+ var mDow as Array?;
+ var mMon as Array?;
+ var mLabels as Array?;
+ var mMeridiem as Array?;
+ var mDateFmt as String = "$1$ $2$";
+
+ function loadStrings() as Void {
+ mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
+ mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
+ mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
+ mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
+ mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
+ }
+
+ function ready() as Boolean { return mDow != null; }
+
+ // Monkey C has no String.split.
+ function splitCsv(str as String) as Array {
+ var out = [] as Array;
+ var rest = str;
+ var cut = rest.find(",");
+ while (cut != null) {
+ out.add(rest.substring(0, cut) as String);
+ rest = rest.substring(cut + 1, rest.length()) as String;
+ cut = rest.find(",");
+ }
+ out.add(rest);
+ return out;
+ }
+
+ function dow(g as Gregorian.Info) as String {
+ return mDow == null ? "" : mDow[g.day_of_week - 1];
+ }
+
+ function month(g as Gregorian.Info) as String {
+ return mMon == null ? "" : mMon[g.month - 1];
+ }
+
+ function monthDay(g as Gregorian.Info) as String {
+ return Lang.format(mDateFmt, [month(g), g.day.format("%d")]);
+ }
+
+ function meridiem(g as Gregorian.Info) as String {
+ return mMeridiem == null ? "" : mMeridiem[g.hour >= 12 ? 1 : 0];
+ }
+
+ // Short caption printed under a bottom-row value.
+ function label(id as Number) as String {
+ if (mLabels == null) { return ""; }
+ var i = FieldTable.indexOf(id);
+ return i < mLabels.size() ? mLabels[i] : "";
+ }
+
+ // One of these is built per onUpdate and handed to every slot, so the
+ // expensive lookups (forecast, sensor history, user profile) happen at most
+ // once per draw and only if some slot actually asks for them.
+ class Ctx {
+ var greg as Gregorian.Info;
+ var is24 as Boolean = true;
+ var metric as Boolean = true;
+ var fahrenheit as Boolean = false;
+
+ private var mInfo as ActivityMonitor.Info?;
+ private var mStats as System.Stats?;
+ private var mDs as System.DeviceSettings?;
+ private var mAct as Activity.Info?;
+ private var mWx as Weather.CurrentConditions?;
+ private var mFc as Array?;
+ private var mProf as UserProfile.Profile?;
+ private var mGotAct = false;
+ private var mGotWx = false;
+ private var mGotFc = false;
+ private var mGotProf = false;
+
+ function initialize(ds as System.DeviceSettings?, tempUnit as Number) {
+ mDs = ds;
+ greg = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
+ if (ds != null) {
+ is24 = ds.is24Hour;
+ metric = (ds.distanceUnits != System.UNIT_STATUTE);
+ fahrenheit = (tempUnit == 2)
+ || (tempUnit == 0 && ds.temperatureUnits == System.UNIT_STATUTE);
+ }
+ if (tempUnit == 1) { fahrenheit = false; }
+ }
+
+ function ds() as System.DeviceSettings? { return mDs; }
+
+ function info() as ActivityMonitor.Info? {
+ if (mInfo == null) { mInfo = ActivityMonitor.getInfo(); }
+ return mInfo;
+ }
+
+ function stats() as System.Stats? {
+ if (mStats == null) { mStats = System.getSystemStats(); }
+ return mStats;
+ }
+
+ function act() as Activity.Info? {
+ if (!mGotAct) { mAct = Activity.getActivityInfo(); mGotAct = true; }
+ return mAct;
+ }
+
+ function wx() as Weather.CurrentConditions? {
+ if (!mGotWx) { mWx = Weather.getCurrentConditions(); mGotWx = true; }
+ return mWx;
+ }
+
+ function forecast() as Weather.DailyForecast? {
+ if (!mGotFc) { mFc = Weather.getDailyForecast(); mGotFc = true; }
+ if (mFc != null && mFc.size() > 0) { return mFc[0]; }
+ return null;
+ }
+
+ function profile() as UserProfile.Profile? {
+ if (!mGotProf) { mProf = UserProfile.getProfile(); mGotProf = true; }
+ return mProf;
+ }
+
+ function where() as Position.Location? {
+ var w = wx();
+ if (w != null && w.observationLocationPosition != null) {
+ return w.observationLocationPosition;
+ }
+ var a = act();
+ if (a != null && a.currentLocation != null) { return a.currentLocation; }
+ return null;
+ }
+ }
+
+ // ------------------------------------------------------------- helpers
+ function num(v as Numeric?) as String {
+ return v == null ? DASH : v.format("%d");
+ }
+
+ function one(v as Float?) as String {
+ return v == null ? DASH : v.format("%.1f");
+ }
+
+ function clock(m as Time.Moment?, c as Ctx) as String {
+ if (m == null) { return DASH; }
+ return clockInfo(Gregorian.info(m, Time.FORMAT_SHORT), c);
+ }
+
+ function clockInfo(g as Gregorian.Info, c as Ctx) as String {
+ var h = g.hour;
+ if (!c.is24) {
+ if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
+ }
+ return h.format("%d") + ":" + g.min.format("%02d");
+ }
+
+ function offsetClock(minutes as Number, c as Ctx) as String {
+ var m = Time.now().add(new Time.Duration(minutes * 60));
+ return clock(m, c);
+ }
+
+ function temp(celsius as Numeric?, c as Ctx) as String {
+ if (celsius == null) { return DASH; }
+ var t = celsius.toFloat();
+ if (c.fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
+ return t.format("%d") + "°";
+ }
+
+ // ActivityMonitor distances are centimetres.
+ function dist(cm as Numeric?, c as Ctx) as String {
+ if (cm == null) { return DASH; }
+ var km = cm.toFloat() / 100000.0;
+ return (c.metric ? km : km * 0.621371).format("%.1f");
+ }
+
+ function metres(m as Numeric?, c as Ctx) as String {
+ if (m == null) { return DASH; }
+ var v = m.toFloat();
+ return (c.metric ? v : v * 3.28084).format("%d");
+ }
+
+ function rounded(v as Numeric?) as String {
+ if (v == null) { return DASH; }
+ var n = v.toNumber();
+ if (n < 1000) { return n.format("%d"); }
+ return (n / 1000.0).format("%.1f") + "k";
+ }
+
+ function setting(name as String, def as Number) as Number {
+ var v = Application.Properties.getValue(name);
+ if (v == null) { return def; }
+ return v as Number;
+ }
+
+ function settingText(name as String) as String {
+ var v = Application.Properties.getValue(name);
+ if (v == null) { return ""; }
+ return v.toString();
+ }
+
+ // Newest reading out of a SensorHistory iterator, or null.
+ function latest(iter) as Numeric? {
+ if (iter == null) { return null; }
+ var s = iter.next();
+ if (s == null) { return null; }
+ return s.data;
+ }
+
+ function dayOfYear(g as Gregorian.Info) as Number {
+ var cum = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
+ var d = cum[g.month - 1] + g.day;
+ var y = g.year;
+ var leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
+ if (leap && g.month > 2) { d = d + 1; }
+ return d;
+ }
+
+ // ISO 8601 week: weeks start Monday, week 1 holds the first Thursday.
+ function isoWeek(g as Gregorian.Info) as Number {
+ var dow = g.day_of_week - 1; // 0 = Sunday
+ var iso = dow == 0 ? 7 : dow; // 1 = Monday .. 7 = Sunday
+ var w = (dayOfYear(g) - iso + 10) / 7;
+ if (w < 1) { return 52; }
+ if (w > 52) { return 1; }
+ return w;
+ }
+
+ // Days since the new moon of 2000-01-06, folded into one synodic month.
+ function moonAge(g as Gregorian.Info) as Float {
+ var y = g.year;
+ var m = g.month;
+ if (m <= 2) { y = y - 1; m = m + 12; }
+ var a = y / 100;
+ var b = 2 - a + a / 4;
+ var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
+ + g.day + b - 1524.5 + g.hour / 24.0;
+ // Monkey C has no float modulo.
+ var syn = 29.530588853;
+ var x = jd - 2451550.1;
+ var age = x - (x / syn).toNumber() * syn;
+ if (age < 0) { age = age + syn; }
+ return age.toFloat();
+ }
+
+ function windDir(bearingDeg as Numeric?) as String {
+ if (bearingDeg == null) { return DASH; }
+ var pts = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
+ "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
+ var b = bearingDeg.toFloat();
+ while (b < 0) { b = b + 360.0; }
+ var i = ((b + 11.25) / 22.5).toNumber() % 16;
+ return pts[i];
+ }
+
+ // --------------------------------------------------------------- value
+ function value(id as Number, c as Ctx) as String {
+ // ---- date / time
+ if (id == 860) { return clockInfo(c.greg, c); }
+ if (id == 880) {
+ var utc = Gregorian.utcInfo(Time.now(), Time.FORMAT_SHORT);
+ return clockInfo(utc, c);
+ }
+ if (id == 864) { return offsetClock(setting("AltOffset1", 0), c); }
+ if (id == 865) { return offsetClock(setting("AltOffset2", 0), c); }
+ if (id == 855) { return c.greg.day.format("%d"); }
+ if (id == 868) { return c.greg.month.format("%d"); }
+ if (id == 859) { return isoWeek(c.greg).format("%d"); }
+ if (id == 873) { return ((dayOfYear(c.greg) + 6) / 7).format("%d"); }
+ if (id == 870) { return dow(c.greg); }
+ if (id == 856) { return dow(c.greg); }
+ if (id == 853) { return month(c.greg); }
+ if (id == 852) { return monthDay(c.greg); }
+ if (id == 872) { return dow(c.greg) + " " + c.greg.day.format("%d"); }
+ if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); }
+ if (id == 858) { return meridiem(c.greg); }
+
+ // ---- activity
+ var i = c.info();
+ if (id == 1) { return i == null ? DASH : num(i.steps); }
+ if (id == 14) { return i == null ? DASH : rounded(i.steps); }
+ if (id == 707) {
+ if (i == null || i.steps == null || i.stepGoal == null) { return DASH; }
+ var left = i.stepGoal - i.steps;
+ return left > 0 ? left.format("%d") : "0";
+ }
+ if (id == 2) { return i == null ? DASH : num(i.calories); }
+ if (id == 603) {
+ if (i == null || i.calories == null) { return DASH; }
+ var p = c.profile();
+ if (p == null || p.weight == null || p.height == null) { return num(i.calories); }
+ return num(i.calories); // device reports total only
+ }
+ if (id == 150 || id == 152 || id == 154) {
+ if (i == null || i.activeMinutesDay == null) { return DASH; }
+ var am = i.activeMinutesDay;
+ if (id == 152) { return num(am.moderate); }
+ if (id == 154) { return num(am.vigorous); }
+ return num(am.total);
+ }
+ if (id == 151) {
+ if (i == null || i.activeMinutesWeek == null) { return DASH; }
+ return num(i.activeMinutesWeek.total);
+ }
+ if (id == 3) { return i == null ? DASH : dist(i.distance, c); }
+ if (id == 4) { return i == null ? DASH : num(i.floorsClimbed); }
+ if (id == 5) { return i == null ? DASH : num(i.floorsDescended); }
+ if (id == 6) { return i == null ? DASH : metres(i.metersClimbed, c); }
+ if (id == 9) { return i == null ? DASH : dist(i.pushDistance, c); }
+ if (id == 10) { return i == null ? DASH : num(i.pushes); }
+ if (id == 8 || id == 15) {
+ if (i == null || i.moveBarLevel == null) { return DASH; }
+ var lvl = i.moveBarLevel;
+ if (id == 15) { lvl = ActivityMonitor.MOVE_BAR_LEVEL_MAX - lvl; }
+ return lvl.format("%d");
+ }
+ if (id == 13) { return i == null ? DASH : num(i.timeToRecovery); }
+ if (id == 11) { return i == null ? DASH : num(i.respirationRate); }
+ if (id == 12) { return i == null ? DASH : num(i.stressScore); }
+
+ // ---- body
+ if (id == 618) {
+ var a = c.act();
+ if (a != null && a.currentHeartRate != null) { return num(a.currentHeartRate); }
+ var h = ActivityMonitor.getHeartRateHistory(1, true);
+ if (h != null) {
+ var s = h.next();
+ if (s != null && s.heartRate != null
+ && s.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
+ return num(s.heartRate);
+ }
+ }
+ return DASH;
+ }
+ if (id == 276) {
+ var p = c.profile();
+ return (p == null) ? DASH : num(p.restingHeartRate);
+ }
+ if (id == 635) {
+ if (!(SensorHistory has :getOxygenSaturationHistory)) { return DASH; }
+ return num(latest(SensorHistory.getOxygenSaturationHistory({:period => 1})));
+ }
+ if (id == 623) {
+ if (!(SensorHistory has :getBodyBatteryHistory)) { return DASH; }
+ return num(latest(SensorHistory.getBodyBatteryHistory({:period => 1})));
+ }
+ if (id == 281 || id == 742) {
+ var p = c.profile();
+ if (p == null || p.weight == null) { return DASH; }
+ var kg = p.weight.toFloat() / 1000.0;
+ if (id == 281) {
+ return (c.metric ? kg : kg * 2.20462).format("%.1f");
+ }
+ if (p.height == null || p.height <= 0) { return DASH; }
+ var m = p.height.toFloat() / 100.0;
+ return (kg / (m * m)).format("%.1f");
+ }
+ if (id == 277 || id == 278) {
+ var p = c.profile();
+ if (p == null) { return DASH; }
+ return num(id == 278 ? p.vo2maxRunning : p.vo2maxCycling);
+ }
+
+ // ---- system
+ var st = c.stats();
+ if (id == 250) {
+ return st == null || st.battery == null ? DASH
+ : st.battery.format("%d") + "%";
+ }
+ if (id == 251) {
+ if (st == null || !(st has :batteryInDays) || st.batteryInDays == null) {
+ return DASH;
+ }
+ return st.batteryInDays.format("%d") + "d";
+ }
+ if (id == 257) {
+ var pct = (st == null || st.battery == null) ? DASH : st.battery.format("%d");
+ var d = (st == null || !(st has :batteryInDays) || st.batteryInDays == null)
+ ? DASH : st.batteryInDays.format("%d");
+ return pct + "/" + d;
+ }
+ if (id == 252) {
+ if (st == null || !(st has :solarIntensity) || st.solarIntensity == null) {
+ return DASH;
+ }
+ return st.solarIntensity.format("%d") + "%";
+ }
+ var ds = c.ds();
+ if (id == 253) { return ds == null ? DASH : num(ds.alarmCount); }
+ if (id == 255) { return ds == null ? DASH : num(ds.notificationCount); }
+ if (id == 254) {
+ return ds == null ? DASH : (ds.doNotDisturb ? "ON" : "OFF");
+ }
+ if (id == 256) {
+ return ds == null ? DASH : (ds.phoneConnected ? "ON" : "OFF");
+ }
+
+ // ---- environment
+ if (id == 615) {
+ var a = c.act();
+ return (a == null) ? DASH : metres(a.altitude, c);
+ }
+ if (id == 616 || id == 733) {
+ var a = c.act();
+ if (a == null) { return DASH; }
+ var pa = (id == 616) ? a.meanSeaLevelPressure : a.ambientPressure;
+ if (pa == null) { return DASH; }
+ return (pa.toFloat() / 100.0).format("%d");
+ }
+ if (id == 712 || id == 717 || id == 700 || id == 731) {
+ var loc = c.where();
+ if (loc == null) { return DASH; }
+ var now = Time.now();
+ var rise = Weather.getSunrise(loc, now);
+ var set = Weather.getSunset(loc, now);
+ if (id == 712) { return clock(rise, c); }
+ if (id == 717) { return clock(set, c); }
+ if (rise == null || set == null) { return DASH; }
+ var next = (now.compare(rise) < 0) ? rise
+ : ((now.compare(set) < 0) ? set : rise);
+ if (id == 700) { return clock(next, c); }
+ var secs = next.compare(now);
+ if (secs < 0) { secs = secs + 86400; }
+ return (secs / 3600).format("%d") + "h" + ((secs % 3600) / 60).format("%02d");
+ }
+ if (id == 706) { return moonAge(c.greg).format("%.1f"); }
+ if (id == 732) {
+ var age = moonAge(c.greg);
+ var lit = (1.0 - Math.cos(2.0 * Math.PI * age / 29.530588853)) / 2.0;
+ return (lit * 100.0).format("%d") + "%";
+ }
+
+ // ---- weather
+ var w = c.wx();
+ if (id == 306) { return w == null ? DASH : temp(w.temperature, c); }
+ if (id == 300) { return w == null ? DASH : temp(w.feelsLikeTemperature, c); }
+ if (id == 305) { return w == null ? DASH : num(w.relativeHumidity) + "%"; }
+ if (id == 304) {
+ if (w == null || !(w has :precipitationChance)) { return DASH; }
+ return num(w.precipitationChance) + "%";
+ }
+ if (id == 308) {
+ if (w == null || w.windSpeed == null) { return DASH; }
+ var kmh = w.windSpeed.toFloat() * 3.6;
+ return (c.metric ? kmh : kmh * 0.621371).format("%d");
+ }
+ if (id == 307) { return w == null ? DASH : num(w.windBearing); }
+ if (id == 310) { return w == null ? DASH : windDir(w.windBearing); }
+ if (id == 309) {
+ if (w == null || w.observationTime == null) { return DASH; }
+ return clock(w.observationTime, c);
+ }
+ if (id == 301 || id == 302 || id == 639) {
+ var f = c.forecast();
+ if (f == null) { return DASH; }
+ if (id == 301) { return temp(f.highTemperature, c); }
+ if (id == 302) { return temp(f.lowTemperature, c); }
+ return temp(f.highTemperature, c) + "/" + temp(f.lowTemperature, c);
+ }
+
+ // ---- custom
+ if (id == 703) { return settingText("Custom1"); }
+ if (id == 704) { return settingText("Custom2"); }
+ if (id == 705) { return settingText("Custom3"); }
+
+ return "";
+ }
+}
diff --git a/tools/gen_fields.py b/tools/gen_fields.py
new file mode 100644
index 0000000..81f82e8
--- /dev/null
+++ b/tools/gen_fields.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python3
+"""One table -> the field id list, both languages, and the settings XML.
+
+Field ids follow the numbering at https://watchface.io/docs/datafields so the
+two catalogues line up, but only the fields Connect IQ can actually serve from
+the watch are here. Anything needing a web service (OpenWeatherMap,
+StormGlass), a companion phone integration (the week-to-date and 7/28-day
+per-sport aggregates), or another installed app (CGM, hydration, quotes) is
+deliberately absent -- see README section 8 for the full reasoning.
+
+ python3 tools/gen_fields.py
+
+Writes:
+ source/FieldTable.mc ids + short label lookup
+ resources/settings/settings.xml the four field pickers
+ resources/strings/fields.xml English names and labels
+ resources-chn/strings/fields.xml Chinese names and labels
+"""
+import os
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.dirname(HERE)
+
+# id, key, English name, Chinese name, on-face label (en), on-face label (cn)
+FIELDS = [
+ (0, "Off", "Off", "关闭", "", ""),
+
+ # ---- date / time ----------------------------------------------------
+ (860, "Time", "Time", "时间", "TIME", "时间"),
+ (880, "UtcTime", "UTC Time", "UTC 时间", "UTC", "UTC"),
+ (864, "AltTime1", "Alternate Time 1", "第二时区 1", "TZ1", "时区1"),
+ (865, "AltTime2", "Alternate Time 2", "第二时区 2", "TZ2", "时区2"),
+ (851, "DayMonthDay", "Day Month Day", "星期 月 日", "DATE", "日期"),
+ (872, "DayDate", "Day Date", "星期 日", "DATE", "日期"),
+ (852, "MonthDay", "Month Day", "月 日", "DATE", "日期"),
+ (855, "DayOfMonth", "Day of Month", "日", "DAY", "日"),
+ (870, "DayShort", "Day (Tue)", "星期简写", "DAY", "星期"),
+ (856, "DayFull", "Full Day (Tuesday)", "星期全称", "DAY", "星期"),
+ (853, "MonthName", "Month (Jan)", "月份", "MON", "月"),
+ (868, "MonthNum", "Month (6)", "月份数字", "MON", "月"),
+ (858, "Meridiem", "AM/PM", "上午/下午", "", ""),
+ (859, "WeekIso", "Week Number (ISO)", "周数 (ISO)", "WEEK", "周"),
+ (873, "WeekCal", "Week Number", "周数", "WEEK", "周"),
+
+ # ---- activity -------------------------------------------------------
+ (1, "Steps", "Steps", "步数", "STEP", "步数"),
+ (14, "StepsRound", "Steps Rounded", "步数 (取整)", "STEP", "步数"),
+ (707, "StepsToGo", "Steps to Go", "剩余步数", "TO GO", "还差"),
+ (2, "Calories", "Calories", "卡路里", "KCAL", "千卡"),
+ (603, "ActiveCal", "Active Calories", "活动卡路里", "ACAL", "活动"),
+ (150, "ActiveMin", "Active Minutes", "活动分钟", "MIN", "分钟"),
+ (152, "ActiveMod", "Active Min (Moderate)", "中强度分钟", "MOD", "中强度"),
+ (154, "ActiveVig", "Active Min (Vigorous)", "高强度分钟", "VIG", "高强度"),
+ (151, "ActiveWeek", "Active Minutes (Week)", "本周活动分钟", "WEEK", "周分钟"),
+ (3, "Distance", "Distance", "距离", "DST", "距离"),
+ (4, "FloorsUp", "Floors Climbed", "上楼层数", "FLR", "楼层"),
+ (5, "FloorsDown", "Floors Descended", "下楼层数", "DESC", "下楼"),
+ (6, "MetersUp", "Distance Climbed", "爬升高度", "ASC", "爬升"),
+ (9, "PushDist", "Push Distance", "推行距离", "PUSH", "推行"),
+ (10, "Pushes", "Pushes", "推行次数", "PUSH", "推行"),
+
+ # ---- body -----------------------------------------------------------
+ (618, "HeartRate", "Heart Rate", "心率", "HR", "心率"),
+ (276, "RestingHr", "Resting Heart Rate", "静息心率", "RHR", "静息"),
+ (12, "Stress", "Stress Score", "压力指数", "STRS", "压力"),
+ (635, "PulseOx", "Pulse Ox", "血氧", "SPO2", "血氧"),
+ (11, "Respiration", "Respiration Rate", "呼吸频率", "RESP", "呼吸"),
+ (623, "BodyBattery", "Body Battery", "身体电量", "BODY", "体能"),
+ (8, "MoveBar", "Move Bar", "活动条", "MOVE", "活动"),
+ (15, "MoveBarRev", "Move Bar Reversed", "活动条 (反向)", "MOVE", "活动"),
+ (281, "Weight", "Weight", "体重", "WT", "体重"),
+ (742, "Bmi", "BMI", "BMI", "BMI", "BMI"),
+ (13, "Recovery", "Time to Recovery", "恢复时间", "RCVR", "恢复"),
+ (278, "Vo2Run", "VO2 Max (Run)", "最大摄氧量 (跑)", "VO2", "摄氧"),
+ (277, "Vo2Bike", "VO2 Max (Bike)", "最大摄氧量 (骑)", "VO2", "摄氧"),
+
+ # ---- system ---------------------------------------------------------
+ (250, "Battery", "Battery", "电量", "BATT", "电量"),
+ (251, "BatteryDays", "Battery in Days", "剩余天数", "DAYS", "天数"),
+ (257, "BatteryBoth", "Battery / Days", "电量 / 天数", "BATT", "电量"),
+ (253, "Alarms", "Alarms", "闹钟", "ALRM", "闹钟"),
+ (255, "Notifications", "Notifications", "通知", "MSG", "通知"),
+ (254, "DoNotDisturb", "Do Not Disturb", "勿扰模式", "DND", "勿扰"),
+ (256, "Bluetooth", "Bluetooth", "蓝牙", "BT", "蓝牙"),
+ (252, "Solar", "Solar Intensity", "太阳能强度", "SOLR", "太阳能"),
+
+ # ---- environment ----------------------------------------------------
+ (615, "Altitude", "Altitude", "海拔", "ALT", "海拔"),
+ (616, "SeaPressure", "Sea Level Pressure", "海平面气压", "hPa", "气压"),
+ (733, "AmbPressure", "Ambient Pressure", "环境气压", "hPa", "气压"),
+ (712, "Sunrise", "Sunrise", "日出", "RISE", "日出"),
+ (717, "Sunset", "Sunset", "日落", "SET", "日落"),
+ (700, "NextSun", "Next Sun Event", "下一太阳事件", "SUN", "太阳"),
+ (731, "UntilSun", "Time Until Sun Event", "距太阳事件", "SUN", "太阳"),
+ (706, "MoonAge", "Moon Age", "月龄", "MOON", "月龄"),
+ (732, "MoonLit", "Moon Illumination", "月球照明", "MOON", "月相"),
+
+ # ---- weather --------------------------------------------------------
+ (306, "Temp", "Temperature", "气温", "TEMP", "气温"),
+ (639, "TempHiLo", "Temperature High/Low", "最高/最低气温", "HI/LO", "高低温"),
+ (300, "FeelsLike", "Feels Like", "体感温度", "FEEL", "体感"),
+ (301, "TempHigh", "High Temperature", "最高气温", "HIGH", "最高"),
+ (302, "TempLow", "Low Temperature", "最低气温", "LOW", "最低"),
+ (305, "Humidity", "Humidity", "湿度", "HUM", "湿度"),
+ (304, "Precip", "Precipitation %", "降水概率", "RAIN", "降水"),
+ (308, "WindSpeed", "Wind Speed", "风速", "WIND", "风速"),
+ (307, "WindBearing", "Wind Bearing", "风向角", "WIND", "风向"),
+ (310, "WindDir", "Wind Direction", "风向", "WIND", "风向"),
+ (309, "WeatherTime", "Weather Update Time", "天气更新时间", "WX", "更新"),
+
+ # ---- custom ---------------------------------------------------------
+ (703, "Custom1", "Custom Text 1", "自定义文字 1", "", ""),
+ (704, "Custom2", "Custom Text 2", "自定义文字 2", "", ""),
+ (705, "Custom3", "Custom Text 3", "自定义文字 3", "", ""),
+]
+
+SLOTS = [("LeftTop", "Top-Left Field", "左上数据"),
+ ("RightTop", "Top-Right Field", "右上数据"),
+ ("BottomLeft", "Bottom-Left Field", "左下数据"),
+ ("BottomRight", "Bottom-Right Field", "右下数据")]
+
+RING = [(0, "Steps", "Steps", "步数"), (1, "Calories", "Calories", "卡路里"),
+ (2, "Distance", "Distance", "距离"), (3, "Floors", "Floors", "楼层"),
+ (4, "MoveBar", "Move Bar", "动动条"), (5, "ActiveMin", "Active Minutes", "活动分钟"),
+ (6, "BodyBattery", "Body Battery", "身体电量")]
+
+OTHER_SETTINGS = """
+
+
+ @Strings.TempUnitAuto
+ @Strings.TempUnitC
+ @Strings.TempUnitF
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+def field_list(indent=8):
+ pad = " " * indent
+ return "\n".join('%s@Strings.Fld%s'
+ % (pad, fid, key) for fid, key, _, _, _, _ in FIELDS)
+
+
+def write(path, text):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ open(path, "w").write(text)
+ print("wrote", os.path.relpath(path, ROOT))
+
+
+def main():
+ ids = [f[0] for f in FIELDS]
+ assert len(ids) == len(set(ids)), "duplicate field id"
+
+ # ---- FieldTable.mc ----
+ L = ["// AUTO-GENERATED by tools/gen_fields.py -- do not edit by hand.",
+ "// Ids follow https://watchface.io/docs/datafields; only the fields a",
+ "// Connect IQ watch face can source on-device are listed.",
+ "import Toybox.Lang;", "", "module FieldTable {", "",
+ " // Field ids, in the same order as the Labels resource string.",
+ " const IDS = ["]
+ row = " "
+ for i, fid in enumerate(ids):
+ piece = "%d%s" % (fid, "," if i < len(ids) - 1 else "")
+ if len(row) + len(piece) > 92:
+ L.append(row)
+ row = " "
+ row += piece
+ L += [row, " ];", "",
+ " function indexOf(id as Number) as Number {",
+ " for (var i = 0; i < IDS.size(); i++) {",
+ " if (IDS[i] == id) { return i; }",
+ " }",
+ " return 0;",
+ " }",
+ "}"]
+ write(os.path.join(ROOT, "source", "FieldTable.mc"), "\n".join(L) + "\n")
+
+ # ---- settings.xml ----
+ S = ['', '',
+ ' ',
+ ' ']
+ names = ["Ember", "Aurora", "Brass", "Voltage", "Reef", "Acid", "Kelp"]
+ for i, n in enumerate(names):
+ S.append(' @Strings.Theme%s' % (i + 1, n))
+ S += [' ', ' ', '']
+ for slot, _, _ in SLOTS:
+ S += [' ' % (slot, slot),
+ ' ', field_list(12),
+ ' ', ' ', '']
+ for ring in ("RingTR", "RingTL", "RingBR", "RingBL"):
+ S += [' ' % (ring, ring),
+ ' ']
+ for rid, key, _, _ in RING:
+ S.append(' @Strings.Metric%s' % (rid, key))
+ S += [' ', ' ', '']
+ S.append(OTHER_SETTINGS.rstrip())
+ S.append('')
+ write(os.path.join(ROOT, "resources", "settings", "settings.xml"), "\n".join(S) + "\n")
+
+ # ---- field strings, both languages ----
+ for lang, folder, ni, li in (("en", "resources", 2, 4), ("chn", "resources-chn", 3, 5)):
+ X = ['']
+ X.append(' ')
+ for f in FIELDS:
+ X.append(' %s' % (f[1], f[ni]))
+ X.append('')
+ X.append(' ')
+ X.append(' %s'
+ % ",".join(f[li] for f in FIELDS))
+ X.append('')
+ for rid, key, en, cn in RING:
+ X.append(' %s' % (key, en if lang == "en" else cn))
+ X.append('')
+ write(os.path.join(ROOT, folder, "strings", "fields.xml"), "\n".join(X) + "\n")
+
+ print("%d fields, %d slots" % (len(FIELDS), len(SLOTS)))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/gen_icons.py b/tools/gen_icons.py
new file mode 100644
index 0000000..69e5ce5
--- /dev/null
+++ b/tools/gen_icons.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+"""Rasterise the design's icon paths into tintable bitmap resources.
+
+The sunrise and weather icons in design/*.svg are traced outlines whose finest
+features are two or three design pixels across. Redrawing them from circles
+and lines works at 454 px and falls apart at 280 px, where those features land
+under one device pixel. Rendering the real paths once per screen size and
+blitting them with drawBitmap2(:tintColor) keeps them exact everywhere.
+
+ python3 tools/gen_icons.py
+
+Writes resources-icons-/drawables/{sunrise,sunset,weather}.png plus the
+matching drawables.xml. White pixels, alpha coverage; the theme's accent
+colour is applied at draw time via :tintColor.
+
+MIP devices have no alpha blending, so for those widths the coverage mask is
+thresholded to hard edges rather than left antialiased -- a soft edge there
+would be quantised to something muddier than a clean one.
+"""
+import os
+import re
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.dirname(HERE)
+SRC = os.path.join(ROOT, "design", "watchface-01-ember.svg")
+
+try:
+ from PIL import Image
+except ImportError:
+ raise SystemExit("this generator needs Pillow: python3 -m pip install pillow")
+
+# The three icons the comps actually contain, and the design-space centre each
+# one is drawn around. A common 32x32 design box keeps blitting trivial.
+BOX = 32.0
+ICONS = {
+ "sunrise": ("sunrise-icon", 156.32, 89.5, False),
+ "sunset": ("sunrise-icon", 156.32, 89.5, True), # same art, chevron flipped
+ "weather": ("weather-icon", 343.55, 89.5, False),
+}
+
+# screen width -> whether the panel can blend alpha (MIP cannot)
+WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True}
+
+SS = 8 # supersampling factor
+
+
+def subpaths(svg, pid):
+ d = re.search(r'= 3:
+ out.append(pts)
+ return out
+
+
+def flip_chevron(paths):
+ """Mirror the horizon bar so its notch points up -- our sunset variant."""
+ out = []
+ for p in paths:
+ ys = [q[1] for q in p]
+ if min(ys) > 96.0: # the bar is the lowest subpath
+ mid = (min(ys) + max(ys)) / 2.0
+ p = [(x, 2 * mid - y) for x, y in p]
+ out.append(p)
+ return out
+
+
+def scanline_evenodd(paths, size, ox, oy, scale):
+ """Even-odd fill at supersampled resolution, returned as a coverage mask."""
+ n = size * SS
+ cov = [bytearray(n) for _ in range(n)]
+ edges = []
+ for p in paths:
+ for i in range(len(p)):
+ x0, y0 = p[i]
+ x1, y1 = p[(i + 1) % len(p)]
+ X0 = (x0 - ox) * scale * SS
+ Y0 = (y0 - oy) * scale * SS
+ X1 = (x1 - ox) * scale * SS
+ Y1 = (y1 - oy) * scale * SS
+ if Y0 != Y1:
+ edges.append((Y0, Y1, X0, X1))
+ for row in range(n):
+ yc = row + 0.5
+ xs = []
+ for Y0, Y1, X0, X1 in edges:
+ if (Y0 <= yc < Y1) or (Y1 <= yc < Y0):
+ xs.append(X0 + (yc - Y0) * (X1 - X0) / (Y1 - Y0))
+ if not xs:
+ continue
+ xs.sort()
+ line = cov[row]
+ for i in range(0, len(xs) - 1, 2):
+ a = max(0, int(round(xs[i])))
+ b = min(n, int(round(xs[i + 1])))
+ for x in range(a, b):
+ line[x] = 255
+ return cov
+
+
+def render(paths, size, cx, cy, blend):
+ scale = size / BOX
+ ox, oy = cx - BOX / 2, cy - BOX / 2
+ cov = scanline_evenodd(paths, size, ox, oy, scale)
+ n = size * SS
+ big = Image.frombytes("L", (n, n), b"".join(bytes(r) for r in cov))
+ small = big.resize((size, size), Image.LANCZOS)
+ if not blend:
+ small = small.point(lambda v: 255 if v >= 110 else 0)
+ white = Image.new("RGB", (size, size), (255, 255, 255))
+ img = white.convert("RGBA")
+ img.putalpha(small)
+ return img
+
+
+# Launcher icon sizes the target devices ask for.
+LAUNCHER = [40, 56, 60, 65, 70]
+
+
+def launcher(size):
+ """A miniature of the face: the gradient ring plus the date band."""
+ from PIL import ImageDraw
+ ss = 8
+ n = size * ss
+ img = Image.new("RGBA", (n, n), (0, 0, 0, 0))
+ d = ImageDraw.Draw(img)
+ d.ellipse([0, 0, n - 1, n - 1], fill=(0, 0, 0, 255))
+ svg = open(SRC).read()
+ top = re.findall(r'fill="(#[0-9A-Fa-f]{6})"',
+ re.search(r'(.*?)', svg, re.S).group(1))
+ bot = re.findall(r'fill="(#[0-9A-Fa-f]{6})"',
+ re.search(r'(.*?)', svg, re.S).group(1))
+ ro, ri = n * 0.495, n * 0.375
+ box_o = [n / 2 - ro, n / 2 - ro, n / 2 + ro, n / 2 + ro]
+ # sweep the whole rim, mirroring the comp's two ramps into four quadrants
+ for q, ramp in ((0, top), (1, top), (2, bot), (3, bot)):
+ for i, c in enumerate(ramp):
+ col = tuple(int(c[1 + 2 * j:3 + 2 * j], 16) for j in range(3)) + (255,)
+ span = 90.0 / len(ramp)
+ if q == 0:
+ a0 = -90 + i * span
+ elif q == 1:
+ a0 = -90 - (i + 1) * span
+ elif q == 2:
+ a0 = 90 - (i + 1) * span
+ else:
+ a0 = 90 + i * span
+ d.pieslice(box_o, a0, a0 + span, fill=col)
+ d.ellipse([n / 2 - ri, n / 2 - ri, n / 2 + ri, n / 2 + ri], fill=(0, 0, 0, 255))
+ band = tuple(int("FEAA00"[2 * j:2 * j + 2], 16) for j in range(3)) + (255,)
+ d.rectangle([0, n * 0.44, n, n * 0.60], fill=band)
+ mask = Image.new("L", (n, n), 0)
+ ImageDraw.Draw(mask).ellipse([0, 0, n - 1, n - 1], fill=255)
+ img.putalpha(mask)
+ return img.resize((size, size), Image.LANCZOS)
+
+
+def main():
+ svg = open(SRC).read()
+ for size in LAUNCHER:
+ outdir = os.path.join(ROOT, "resources-launcher-%d" % size, "drawables")
+ os.makedirs(outdir, exist_ok=True)
+ launcher(size).save(os.path.join(outdir, "launcher_icon.png"))
+ with open(os.path.join(outdir, "drawables.xml"), "w") as f:
+ f.write('\n'
+ ' \n'
+ '\n')
+ print("wrote %s (%dx%d)" % (outdir, size, size))
+ cache = {}
+ for width, blend in sorted(WIDTHS.items()):
+ size = int(round(BOX * width / 500.0))
+ outdir = os.path.join(ROOT, "resources-icons-%d" % width, "drawables")
+ os.makedirs(outdir, exist_ok=True)
+ names = []
+ for name, (pid, cx, cy, flip) in ICONS.items():
+ key = (pid, flip)
+ if key not in cache:
+ p = subpaths(svg, pid)
+ cache[key] = flip_chevron(p) if flip else p
+ img = render(cache[key], size, cx, cy, blend)
+ img.save(os.path.join(outdir, "%s.png" % name))
+ names.append(name)
+ with open(os.path.join(outdir, "drawables.xml"), "w") as f:
+ f.write("\n")
+ for n in names:
+ # packingFormat="png" keeps the alpha channel instead of
+ # collapsing the image to a palette; drawBitmap2(:tintColor)
+ # refuses a palettised source.
+ f.write(' \n' % (n.capitalize(), n))
+ f.write("\n")
+ print("wrote %s (%dx%d, %s edges)"
+ % (outdir, size, size, "soft" if blend else "hard"))
+
+
+if __name__ == "__main__":
+ main()