├── TH3-screenshot.png ├── resources ├── bklight.png ├── ico_cal.png ├── ico_active.png ├── ico_azonem.png ├── ico_batt.png ├── ico_climb.png ├── ico_dist.png ├── ico_steps.png ├── widget.defs ├── widgets.gui ├── styles.css ├── styles~348x250.css ├── styles~336x336.css ├── index.gui └── index.view ├── README.md ├── LICENSE ├── companion └── index.js ├── settings └── index.jsx └── app └── index.js /TH3-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/TH3-screenshot.png -------------------------------------------------------------------------------- /resources/bklight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/bklight.png -------------------------------------------------------------------------------- /resources/ico_cal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_cal.png -------------------------------------------------------------------------------- /resources/ico_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_active.png -------------------------------------------------------------------------------- /resources/ico_azonem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_azonem.png -------------------------------------------------------------------------------- /resources/ico_batt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_batt.png -------------------------------------------------------------------------------- /resources/ico_climb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_climb.png -------------------------------------------------------------------------------- /resources/ico_dist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_dist.png -------------------------------------------------------------------------------- /resources/ico_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyuen/THE-watchface/HEAD/resources/ico_steps.png -------------------------------------------------------------------------------- /resources/widget.defs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/widgets.gui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THE watchface for Fitbit Ionic/Versa/Versa Lite/Versa 3/Sense 2 | 3 | ![screenshot](TH3-screenshot.png) 4 | 5 | Features: 6 | - Customize the color of the minute hand and rings 7 | - Tap to cycle between the numerical stats 8 | - Customize which numerical stats are shown 9 | - 4 rings to show steps, distance, calories and floors climbs (becomes active minutes on Versa Lite) 10 | - Battery indicator under the week name 11 | - Direct link to app store (open with phone): 12 | https://gam.fitbit.com/gallery/clock/cc129d5e-ec49-40f0-8f2e-7f5f08ab3095 13 | -------------------------------------------------------------------------------- /resources/styles.css: -------------------------------------------------------------------------------- 1 | line { 2 | stroke-linecap: round; 3 | } 4 | #mydate { 5 | font-size: 26; 6 | font-family: System-Regular; 7 | text-anchor: middle; 8 | text-length: 2; 9 | x: 82%; 10 | y: 50%+10; 11 | } 12 | 13 | #myweek { 14 | font-size: 26; 15 | font-family: System-Regular; 16 | text-anchor: middle; 17 | text-length: 20; 18 | x: 23%; 19 | y: 50%+10; 20 | } 21 | 22 | #mystats { 23 | font-size: 32; 24 | font-family: System-Bold; 25 | text-anchor: middle; 26 | text-length: 12; 27 | y: 76%+10; 28 | } 29 | #batt_bars { 30 | transform: translate(23%,0); 31 | } 32 | #batt0 { 33 | stroke-linecap: butt; 34 | y1: 50%+19; 35 | y2: 50%+19; 36 | } 37 | #batt { 38 | stroke-linecap: butt; 39 | y1: 50%+19; 40 | y2: 50%+19; 41 | } 42 | -------------------------------------------------------------------------------- /resources/styles~348x250.css: -------------------------------------------------------------------------------- 1 | line { 2 | stroke-linecap: round; 3 | } 4 | #mydate { 5 | font-size: 24; 6 | font-family: System-Regular; 7 | text-anchor: middle; 8 | text-length: 2; 9 | x: 73%; 10 | y: 50%+8; 11 | } 12 | 13 | #myweek { 14 | font-size: 24; 15 | font-family: System-Regular; 16 | text-anchor: middle; 17 | text-length: 20; 18 | x: 31%; 19 | y: 50%+8; 20 | } 21 | 22 | #mystats { 23 | font-size: 30; 24 | font-family: System-Bold; 25 | text-anchor: middle; 26 | text-length: 12; 27 | y: 76%+8; 28 | } 29 | #batt_bars { 30 | transform: translate(31%,0); 31 | } 32 | #batt0 { 33 | stroke-linecap: butt; 34 | y1: 50%+17; 35 | y2: 50%+17; 36 | } 37 | #batt { 38 | stroke-linecap: butt; 39 | y1: 50%+17; 40 | y2: 50%+17; 41 | } 42 | -------------------------------------------------------------------------------- /resources/styles~336x336.css: -------------------------------------------------------------------------------- 1 | #face { 2 | x: 18; 3 | y: 18; 4 | width: 100%-36; 5 | height: 100%-36; 6 | } 7 | 8 | #corners { 9 | x: 24; 10 | y: 24; 11 | width: 100%-48; 12 | height: 100%-48; 13 | } 14 | 15 | line { 16 | stroke-linecap: round; 17 | } 18 | 19 | #mydate { 20 | font-size: 26; 21 | font-family: System-Regular; 22 | text-anchor: middle; 23 | text-length: 2; 24 | x: 82%; 25 | y: 50%+10; 26 | } 27 | 28 | #myweek { 29 | font-size: 26; 30 | font-family: System-Regular; 31 | text-anchor: middle; 32 | text-length: 20; 33 | x: 23%; 34 | y: 50%+10; 35 | } 36 | 37 | #mystats { 38 | font-size: 32; 39 | font-family: System-Bold; 40 | text-anchor: middle; 41 | text-length: 12; 42 | y: 76%+10; 43 | } 44 | #batt_bars { 45 | transform: translate(23%,0); 46 | } 47 | #batt0 { 48 | stroke-linecap: butt; 49 | y1: 50%+19; 50 | y2: 50%+19; 51 | } 52 | #batt { 53 | stroke-linecap: butt; 54 | y1: 50%+19; 55 | y2: 50%+19; 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Terry Yuen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /companion/index.js: -------------------------------------------------------------------------------- 1 | import {settingsStorage as store} from "settings"; 2 | import {peerSocket} from "messaging"; 3 | import {me} from "companion"; 4 | import {outbox} from "file-transfer"; 5 | import {encode} from "cbor"; 6 | import {locale} from "user-settings"; 7 | import {device} from "peer"; 8 | 9 | store.onchange = sendAll; 10 | 11 | peerSocket.onmessage = e => { 12 | if(e.data && e.data.getAll) sendAll(); 13 | }; 14 | 15 | if(me.launchReasons.settingsChanged) sendAll(); 16 | 17 | function sendAll() { 18 | let obj = { 19 | theme: trim(store.getItem("theme") || "blue"), 20 | hideRings: (store.getItem("hideRings") === "true"), 21 | unboldStats: (store.getItem("unboldStats") === "true"), 22 | stats: ["none", "steps", "heart", "floors", "cals", "mins", "time"], 23 | firstStat: 0, 24 | days: getLocale() 25 | }; 26 | if(store.getItem("stats")) { 27 | obj.stats = JSON.parse(store.getItem("stats")).values.map(n => n.value); 28 | } 29 | if(store.getItem("firstStat2")) { 30 | let value = JSON.parse(store.getItem("firstStat2")).values[0].value; 31 | for(let i = 0; i < obj.stats.length; i++) { 32 | if(value === obj.stats[i]) { 33 | obj.firstStat = i; 34 | break; 35 | } 36 | } 37 | } 38 | outbox.enqueue("settings2.txt", encode(obj)); 39 | } 40 | 41 | function trim(s) { 42 | return (s.charAt && s.charAt(0) === '"') ? s.substr(1, s.length - 2) : s; 43 | } 44 | 45 | function getLocale() { 46 | try { 47 | new Date().toLocaleDateString("i"); 48 | } catch(e) { 49 | let lang = locale.language.replace("_", "-"); 50 | let days = []; 51 | for(let i = 0; i < 7; i++) { 52 | days.push(new Date(2000, 0, i + 2).toLocaleDateString(lang, {weekday: "short"}).toUpperCase().replace(".", "")); 53 | } 54 | return days; 55 | } 56 | } 57 | 58 | if(store.getItem("modelName") !== device.modelName) store.setItem("modelName", device.modelName); 59 | -------------------------------------------------------------------------------- /settings/index.jsx: -------------------------------------------------------------------------------- 1 | function isNotEmpty(list) { 2 | list = list && JSON.parse(list); 3 | list = list && list.values; 4 | return list && (list.length === 1 ? list[0].value !== "none" : list.length > 1); 5 | } 6 | 7 | //stats = {selected: [1, 0, 2], values[{name:"None",value:"none"},{name:"Steps",value:"steps"},{name:"Heart Rate (bpm)",value:"heart"}]} 8 | 9 | //firstStat = {"values":[{"name":"Steps","value":"steps"}],"selected":[1]} 10 | 11 | function getClampedFirstStat(props) { 12 | let first = props.settingsStorage.getItem("firstStat2"); 13 | let stats = props.settingsStorage.getItem("stats"); 14 | if(first && stats) { 15 | return Math.min(JSON.parse(first).selected[0], JSON.parse(stats).selected.length - 1); 16 | } 17 | } 18 | 19 | registerSettingsPage(props => { 20 | let statsList = [ 21 | {name: "None", value: "none"}, 22 | {name: "Steps", value: "steps"}, 23 | {name: "Heart Rate (bpm)", value: "heart"}, 24 | {name: "Resting Heart Rate (- bpm -)", value: "rest"}, 25 | {name: "Distance (km/mi)", value: "dist"}, 26 | {name: "Floors Climbed (f)", value: "floors"}, 27 | {name: "Calories Burned (cal)", value: "cals"}, 28 | {name: "Active Zone Minutes (hh'mm'')", value: "mins"}, 29 | {name: "Weight (kg/lb)", value: "weight"}, 30 | {name: "Digital Time", value: "time"}, 31 | {name: "Battery", value: "batt"} 32 | ]; 33 | 34 | if(/Versa Lite/.test(props.settings.modelName)) { 35 | statsList = statsList.filter(n => n.value !== "floors"); 36 | } 37 | 38 | return ( 39 | 40 |
41 | 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | props.settingsStorage.setItem("firstStat2", JSON.stringify(sel))} 75 | selected={getClampedFirstStat(props)} 76 | /> : null)} 77 | 78 | 79 |
80 |
81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /resources/index.gui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /resources/index.view: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import clock from "clock"; 2 | import document from "document"; 3 | import * as health from "user-activity"; 4 | import {HeartRateSensor} from "heart-rate"; 5 | import {display} from "display"; 6 | import {vibration} from "haptics"; 7 | import {peerSocket} from "messaging"; 8 | import {preferences, units} from "user-settings"; 9 | import {me} from "appbit"; 10 | import {user} from "user-profile"; 11 | import * as fs from "fs"; 12 | import {battery} from "power"; 13 | import {decode} from "cbor"; 14 | import {inbox} from "file-transfer"; 15 | 16 | const THEMES = { 17 | red: ["F93535", "CC4848", "AB4545"], 18 | orange: ["FF970F", "DD7F23", "B3671D"], 19 | yellow: ["FFFF00", "E4DB4A", "C6BC1E"], 20 | green: ["14C610", "119E0E", "0D730B"], 21 | blue: ["6fa8e9", "5682b4", "32547a"], 22 | purple: ["E86FE9", "B455B5", "79327A"], 23 | navy: ["5555ff", "4444ff", "4444ff"], 24 | grey: ["888888", "666666", "444444"], 25 | white: ["FFFFFF", "FFFFFF", "FFFFFF"] 26 | }; 27 | const HOUR12 = (preferences.clockDisplay === "12h"); 28 | const PROFILE = me.permissions.granted("access_user_profile"); 29 | const NOCLIMB = (health.today.local.elevationGain === undefined); 30 | 31 | let weekNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; 32 | 33 | let lastUpdatedRings = 0; 34 | let lastUpdatedHeart = 0; 35 | let showRings = true; 36 | let unboldStats = false; 37 | 38 | let stats = NOCLIMB ? ["none", "steps", "heart", "cals", "mins", "time"] : ["none", "steps", "heart", "floors", "cals", "mins", "time"]; 39 | let firstStat = 0; //0=blank 40 | let curStat = 0; 41 | let heartSensor; 42 | 43 | let myDate = $("mydate"); 44 | let myWeek = $("myweek"); 45 | let myHours = $("hours"); 46 | let myMins = $("minutes"); 47 | let mySecs = $("seconds"); 48 | let myStats = $("mystats"); 49 | let myBatt = $("batt"); 50 | 51 | let myRingTL = $("today_tl"); 52 | let myRingTR = $("today_tr"); 53 | let myRingBL = $("today_bl"); 54 | let myRingBR = $("today_br"); 55 | 56 | function $(s) { 57 | return document.getElementById(s); 58 | } 59 | 60 | function onTick(now) { 61 | now = (now && now.date) || new Date(); 62 | myDate.text = now.getDate(); 63 | myWeek.text = weekNames[now.getDay()]; 64 | 65 | let hours = now.getHours() % 12; 66 | let mins = now.getMinutes(); 67 | let secs = now.getSeconds(); 68 | myHours.groupTransform.rotate.angle = (hours + mins/60)*30; 69 | myMins.groupTransform.rotate.angle = mins*6; 70 | mySecs.groupTransform.rotate.angle = secs*6; 71 | 72 | myBatt.x2 = Math.round(battery.chargeLevel*7/25) - 14; 73 | 74 | if(showRings || (stats.length > 0 && stats[curStat] !== "none")) { 75 | let nowTime = now.getTime(); 76 | 77 | if(showRings) { 78 | if(nowTime - lastUpdatedRings > 30000) { 79 | lastUpdatedRings = nowTime; 80 | let today = health.today.adjusted; 81 | let goal = health.goals; 82 | updateRing(myRingTL, "cal", goal, today); 83 | updateRing(myRingTR, "step", goal, today); 84 | updateRing(myRingBR, "dist", goal, today); 85 | if(NOCLIMB) { 86 | updateRing(myRingBL, "active", goal, today); 87 | } else { 88 | updateRing(myRingBL, "climb", goal, today); 89 | } 90 | } 91 | } 92 | 93 | if(stats.length > 0 && stats[curStat] !== "none" && !display.aodActive) { 94 | if(stats[curStat] !== "heart") { 95 | updateStat(); 96 | } else { 97 | if(nowTime - lastUpdatedHeart > 1600) { 98 | lastUpdatedHeart = nowTime; 99 | updateHeart(); 100 | } 101 | } 102 | } else { 103 | myStats.text = ""; 104 | } 105 | } 106 | } 107 | 108 | function setAOD(on) { 109 | if(on) { 110 | clock.granularity = "minutes"; 111 | mySecs.style.display = "none"; 112 | myStats.style.display = "none"; 113 | } else { 114 | clock.granularity = "seconds"; 115 | mySecs.style.display = "inline"; 116 | myStats.style.display = "inline"; 117 | } 118 | } 119 | 120 | if(display.aodAvailable && me.permissions.granted("access_aod")) { 121 | display.aodAllowed = true; 122 | display.onchange = () => { 123 | setAOD(display.aodActive); 124 | if(!display.aodActive) onTick(); 125 | }; 126 | setAOD(display.aodActive); 127 | } else { 128 | clock.granularity = "seconds"; 129 | } 130 | 131 | clock.ontick = onTick; 132 | onTick(); 133 | 134 | $("top_half").onclick = () => { 135 | if(!display.aodEnabled) { 136 | if(display.autoOff === true) { 137 | display.autoOff = false; 138 | if(!display.autoOff) $("bklight").style.display = "inline"; 139 | } else { 140 | display.autoOff = true; 141 | if(display.autoOff) $("bklight").style.display = "none"; 142 | } 143 | } 144 | }; 145 | 146 | $("btm_half").onclick = () => { 147 | if(stats.length > 0) { 148 | curStat = (curStat + 1) % stats.length; 149 | if(stats[curStat] === "heart") { 150 | updateHeart(); 151 | } else { 152 | updateStat(); 153 | } 154 | } 155 | }; 156 | 157 | function updateRing(node, holder, goal, today) { 158 | let angle = 0; 159 | if(holder === "cal") { 160 | angle = (today.calories || 0)*360/(goal.calories || 400); 161 | } else if(holder === "step") { 162 | angle = (today.steps || 0)*360/(goal.steps || 10000); 163 | } else if(holder === "dist") { 164 | angle = (today.distance || 0)*360/(goal.distance || 7200); 165 | } else if(holder === "climb") { 166 | angle = (today.elevationGain || 0)*360/(goal.elevationGain || 20); 167 | } else if(holder === "active") { 168 | angle = (today.activeZoneMinutes.total || 0)*360/(goal.activeZoneMinutes.total || 30); 169 | } 170 | node.sweepAngle = Math.min(360, Math.round(angle)); 171 | } 172 | 173 | function updateStat() { 174 | let today = health.today.adjusted; 175 | switch(stats[curStat]) { 176 | case "steps": 177 | myStats.text = today.steps; break; 178 | case "heart": 179 | break; 180 | case "dist": 181 | myStats.text = (units.distance === "metric") ? round(today.distance/1000) + " km" : round(today.distance/1609.34) + " mi"; 182 | break; 183 | case "floors": 184 | myStats.text = today.elevationGain + " f"; 185 | break; 186 | case "cals": 187 | myStats.text = today.calories + " cal"; 188 | break; 189 | case "mins": 190 | let t = today.activeZoneMinutes.total; 191 | myStats.text = Math.floor(t/60) + "' " + pad(t % 60) + '"'; 192 | break; 193 | case "time": 194 | let t = new Date(); 195 | let hr = t.getHours(); 196 | myStats.text = ((hr > 12 && HOUR12) ? hr % 12 : hr) + ":" + pad(t.getMinutes()); 197 | break; 198 | case "weight": 199 | myStats.text = !PROFILE ? "No Access" : (units.weight === "metric" ? round(user.weight) + " kg" : round(user.weight/2.2046) + " lb"); 200 | break; 201 | case "rest": 202 | myStats.text = !PROFILE ? "No Access" : "- " + user.restingHeartRate + " -"; 203 | break; 204 | case "batt": 205 | myStats.text = battery.chargeLevel + "%"; 206 | break; 207 | default: myStats.text = ""; 208 | } 209 | } 210 | 211 | function pad(n) { 212 | return n < 10 ? "0" + n : n; 213 | } 214 | 215 | function round(n) { 216 | n = n.toFixed(2); 217 | if(n.substr(-2) === "00") return n.substr(0, n.length - 3); 218 | if(n.substr(-1) === "0") return n.substr(0, n.length - 1); 219 | return n; 220 | } 221 | 222 | var delayHeart; 223 | 224 | function updateHeart() { 225 | let h = heartSensor; 226 | if(!h) { 227 | heartSensor = h = new HeartRateSensor(); 228 | h.onreading = () => { 229 | setTimeout(() => h.stop(), 100); 230 | clearTimeout(delayHeart); 231 | myStats.text = h.heartRate; 232 | }; 233 | h.onerror = () => { 234 | setTimeout(() => h.stop(), 100); 235 | clearTimeout(delayHeart); 236 | myStats.text = "--"; 237 | }; 238 | } 239 | if(!h.activated) { 240 | clearTimeout(delayHeart); 241 | delayHeart = setTimeout(() => { 242 | myStats.text = "--"; 243 | }, 500); 244 | h.start(); 245 | } 246 | } 247 | 248 | function applySettings(o) { 249 | if(o.theme) { 250 | let colors = THEMES[o.theme] || []; 251 | for(let i = 0; i < colors.length; i++) { 252 | let nodes = document.getElementsByClassName("color" + (i + 1)); 253 | let node, j = 0; 254 | while(node = nodes[j++]) node.style.fill = "#" + colors[i]; 255 | } 256 | } 257 | if(o.days) { 258 | weekNames = o.days; 259 | } 260 | if("hideRings" in o) { 261 | showRings = !o.hideRings; 262 | let nodes = document.getElementsByClassName("rings"); 263 | let node, j = 0; 264 | while(node = nodes[j++]) node.style.display = showRings ? "inline" : "none"; 265 | } 266 | if("unboldStats" in o) { 267 | unboldStats = o.unboldStats; 268 | myStats.style.fontFamily = unboldStats ? "System-Regular" : "System-Bold"; 269 | } 270 | if("stats" in o) stats = o.stats; 271 | if("firstStat" in o) curStat = firstStat = Math.min(o.firstStat, stats.length - 1); 272 | myStats.text = ""; 273 | lastUpdatedRings = 0; 274 | lastUpdatedHeart = 0; 275 | } 276 | 277 | function parseFile(name) { 278 | let obj; 279 | try { 280 | obj = fs.readFileSync(name, "cbor"); 281 | } catch(e) { 282 | return true; 283 | } 284 | 285 | if(name === "settings2.txt") { 286 | if(obj) applySettings(obj); 287 | } 288 | } 289 | 290 | function pendingFiles() { 291 | let found = false; 292 | let temp; 293 | while(temp = inbox.nextFile()) { 294 | parseFile(temp); 295 | found = true; 296 | } 297 | if(found) { 298 | display.poke(); 299 | vibration.start("bump"); 300 | } 301 | } 302 | 303 | pendingFiles(); 304 | inbox.onnewfile = pendingFiles; 305 | 306 | if(parseFile("settings2.txt")) { 307 | let done = (peerSocket.readyState === peerSocket.OPEN); 308 | if(done) { 309 | peerSocket.send({getAll: 1}); 310 | } else { 311 | peerSocket.onopen = () => { 312 | if(!done) peerSocket.send({getAll: 1}); 313 | done = true; 314 | }; 315 | } 316 | } 317 | 318 | if(NOCLIMB) { 319 | $("floors").href = "ico_azonem.png"; 320 | } 321 | --------------------------------------------------------------------------------