diff --git a/apps.json b/apps.json index a99c5df82..9db1ba425 100644 --- a/apps.json +++ b/apps.json @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.40", + "version": "0.41", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -218,7 +218,7 @@ { "id": "locale", "name": "Languages", - "version": "0.14", + "version": "0.15", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", @@ -1717,17 +1717,18 @@ { "id": "wohrm", "name": "Workout HRM", - "version": "0.08", + "version": "0.09", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "icon": "app.png", "type": "app", "tags": "hrm,workout", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], "storage": [ {"name":"wohrm.app.js","url":"app.js"}, + {"name":"wohrm.settings.js","url":"settings.js"}, {"name":"wohrm.img","url":"app-icon.js","evaluate":true} ] }, @@ -1893,13 +1894,15 @@ { "id": "widhwt", "name": "Hand Wash Timer", - "version": "0.01", - "description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.", + "version": "0.02", + "description": "On Bangle.js 1 swipe your wrist over the watch face to start your personal Bangle.js 1 hand wash timer. On Bangle.js2 the Pattern Launcher is recommended to start the timer. Start washing after the short buzz and stop after the long buzz 35sec. later.", "icon": "widget.png", "type": "widget", "tags": "widget,tool", - "supports": ["BANGLEJS"], + "allow_emulator": true, + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ + {"name":"widhwt.app.js","url":"app.js"}, {"name":"widhwt.wid.js","url":"widget.js"} ] }, @@ -3794,7 +3797,7 @@ { "id": "simplest", "name": "Simplest Clock", - "version": "0.03", + "version": "0.05", "description": "The simplest working clock, acts as a tutorial piece", "icon": "simplest.png", "screenshots": [{"url":"screenshot_simplest.png"}], @@ -4215,7 +4218,7 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.10", + "version": "0.11", "description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", "dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"}, @@ -4242,7 +4245,7 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.04", + "version": "0.05", "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "readme":"README.md", "icon": "app.png", @@ -4424,7 +4427,7 @@ "name": "Q Alarm and Timer", "shortName": "Q Alarm", "icon": "app.png", - "version": "0.03", + "version": "0.04", "description": "Alarm and timer app with days of week and 'hard' option.", "tags": "tool,alarm,widget", "supports": ["BANGLEJS", "BANGLEJS2"], @@ -4805,7 +4808,7 @@ { "id": "menuwheel", "name": "Wheel Menus", - "version": "0.01", + "version": "0.02", "description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button", "readme": "README.md", "icon": "icon.png", @@ -5039,9 +5042,10 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.02", + "version":"0.04", "description": "A clock with circles for different data at the bottom in a probably familiar style", "icon": "app.png", + "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}], "dependencies": {"widpedom":"app"}, "type": "clock", "tags": "clock", @@ -5090,6 +5094,24 @@ ] }, { + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "screenshots": [{"url":"screenshot_mmind.png"}], + "type": "app", + "tags": "game", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] + }, + { "id": "presentor", "name": "Presentor", "version": "3.0", @@ -5397,7 +5419,7 @@ }, { "id": "flipper", - "name": "flipper", + "name": "Flipper", "version": "0.01", "description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.", "readme":"README.md", @@ -5425,5 +5447,22 @@ {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} ] + }, + { + "id": "limelight", + "name": "Limelight", + "version": "0.01", + "description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)", + "icon": "limelight.png", + "readme":"README.md", + "screenshots": [{"url":"screenshot_limelight.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"limelight.app.js","url":"limelight.app.js"}, + {"name":"limelight.settings.js","url":"limelight.settings.js"}, + {"name":"limelight.img","url":"limelight.icon.js","evaluate":true} + ] } ] diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index 668047d7a..fdf20c175 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -2,3 +2,6 @@ 0.02: Load widgets after setUI so widclk knows when to hide 0.03: Clock now shows day of week under date. 0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too. +0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off) + when weekday name "Off": week #: + when weekday name "On": weekday name is cut at 6th position and .# is added \ No newline at end of file diff --git a/apps/antonclk/README.md b/apps/antonclk/README.md index 41d3e4559..fa4cc4919 100644 --- a/apps/antonclk/README.md +++ b/apps/antonclk/README.md @@ -40,8 +40,7 @@ The main menu contains several settings covering Anton clock in general. * **Show Weekday** - Weekday is shown in the time presentation without seconds. Weekday name depends on the current locale. If seconds are shown, the weekday is never shown as there is not enough space on the watch face. -* **Uppercase** - Weekday name and month name in the long format are converted to upper case letters. -This can improve readability. +**Show Weeknumber** - Weeknumber (ISO-8601) is shown. * **Vector font** - Use the built-in vector font for dates and weekday. This can improve readability. Otherwise, a scaled version of the built-in 6x8 pixels font is used. diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index 1f3e49792..be6c23789 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -19,6 +19,7 @@ var secondsWithColon; var dateOnMain; var dateOnSecs; var weekDay; +var calWeek; var upperCase; var vectorFont; @@ -29,22 +30,25 @@ var secondsScreen = true; var isBangle1 = (g.getWidth() == 240); -/* For development purposes +//For development purposes +/* require('Storage').writeJSON(SETTINGSFILE, { - secondsMode: "Always", // "Never", "Unlocked", "Always" + secondsMode: "Unlocked", // "Never", "Unlocked", "Always" secondsColoured: true, secondsWithColon: true, dateOnMain: "Long", // "Short", "Long", "ISO8601" dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false weekDay: true, + calWeek: true, upperCase: true, vectorFont: true, }); -/* */ +*/ -/* OR (also for development purposes) +// OR (also for development purposes) +/* require('Storage').erase(SETTINGSFILE); -/* */ +*/ // Helper method for loading the settings function def(value, def) { @@ -60,6 +64,7 @@ function loadSettings() { dateOnMain = def(settings.dateOnMain, "Long"); dateOnSecs = def(settings.dateOnSecs, "Year"); weekDay = def(settings.weekDay, true); + calWeek = def(settings.calWeek, false); upperCase = def(settings.upperCase, true); vectorFont = def(settings.vectorFont, false); @@ -99,6 +104,18 @@ function isoStr(date) { return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).substr(-2) + "-" + ("0" + date.getDate()).substr(-2); } +function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + return 1 + Math.ceil((firstThursday - tdt) / 604800000); +} + function doColor() { return !isBangle1 && !Bangle.isLocked() && secondsColoured; } @@ -169,11 +186,14 @@ function draw() { else g.setFont("6x8", 2); g.drawString(dateStr, x, y); - if (weekDay) { - var dowStr = require("locale").dow(date); + if (weekDay || calWeek) { + var dowwumStr = require("locale").dow(date); + dowwumStr = "thursday"; + if (calWeek) + dowwumStr = (weekDay ? dowwumStr.substr(0,Math.min(dowwumStr.length,6)) + (dowwumStr.length>=6 ? "." : "") : "week ") + "#" + ISO8601calWeek(date); if (upperCase) - dowStr = dowStr.toUpperCase(); - g.drawString(dowStr, x, y + (vectorFont ? 26 : 16)); + dowwumStr = dowwumStr.toUpperCase(); + g.drawString(dowwumStr, x, y + (vectorFont ? 26 : 16)); } } diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js index 08fde512e..293aa0438 100644 --- a/apps/antonclk/settings.js +++ b/apps/antonclk/settings.js @@ -47,6 +47,14 @@ writeSettings(); } }, + "Show Weeknumber": { + value: (settings.weekNum !== undefined ? settings.weekNum : true), + format: v => v ? "On" : "Off", + onchange: v => { + settings.weekNum = v; + writeSettings(); + } + }, "Uppercase": { value: (settings.upperCase !== undefined ? settings.upperCase : false), format: v => v ? "On" : "Off", diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index d6619822b..702a8091e 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -44,3 +44,4 @@ 0.38: Option to log to file if settings.log==2 0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035) 0.40: Bootloader now rebuilds for new firmware versions +0.41: Add Keyboard and Mouse Bluetooth HID option diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 664d64ee7..1b826de5a 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -18,6 +18,7 @@ boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; if (s.ble!==false) { if (s.HID) { // Human interface device if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; + else if (s.HID=="com") boot += `Bangle.HID = E.toUint8Array(atob("BQEJAqEBhQEJAaEABQkZASkFFQAlAZUFdQGBApUBdQOBAwUBCTAJMQk4FYElf3UIlQOBBgUMCjgCFYElf3UIlQGBBsDABQEJBqEBhQIFBxngKecVACUBdQGVCIECdQiVAYEBGQApcxUAJXOVBXUIgQDA"));` else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));` else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`; boot += `bleServiceOptions.hid=Bangle.HID;\n`; diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index c0aa4e2f8..ca1da6e21 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -1,3 +1,7 @@ 0.01: New clock 0.02: Fix icon & add battery warn functionality 0.03: Theming support & minor fixes +0.04: Make configurable what to show in each circle + Add step distance and weather + Allow switching visibility of widgets + Make circles and text slightly bigger diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 66d9afe08..9aaa4bc8e 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -2,19 +2,22 @@ A clock with circles for different data at the bottom in a probably familiar style -It shows besides time, date and day of week the following information: +By default the time, date and day of week is shown. + +It can show the following information (this can be configured): * Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer)) - * Heart rate (when screen is on and unlocked) - * Battery (including charging and battery low) + * Steps distance (depending on steps) + * Heart rate (automatically updates when screen is on and unlocked) + * Battery (including charging status and battery low warning) + * Weather (requires [weather app](https://banglejs.com/apps/#weather)) -## Screenshot +## Screenshots +![Screenshot dark theme](screenshot-dark.png) +![Screenshot light theme](screenshot-light.png) -![Screenshot](screenshot.png) - -## TODO -* Show weather information -* Configure which information to show in each circle -* Configure visibility of widgets +# TODO +* Add sunrise and sunset +* Display moon instead of sun during night on weather circle ## Creator Marco ([myxor](https://github.com/myxor)) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 026b47cc6..91d4937c4 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,19 +1,37 @@ const locale = require("locale"); const heatshrink = require("heatshrink"); +const storage = require("Storage"); const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA=")); +const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD")); const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI")); const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA")); +const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA")); +const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA=")); +const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA==")); +const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg")); +const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA==")); +const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA=")); +const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA=")); +const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA==")); + let settings; function loadSettings() { - settings = require("Storage").readJSON("circlesclock.json", 1) || { + settings = storage.readJSON("circlesclock.json", 1) || { + 'minHR': 40, 'maxHR': 200, 'stepGoal': 10000, - 'batteryWarn': 30 + 'stepDistanceGoal': 8000, + 'stepLength': 0.8, + 'batteryWarn': 30, + 'showWidgets': false, + 'circle1': 'hr', + 'circle2': 'steps', + 'circle3': 'battery' }; // Load step goal from pedometer widget as fallback if (settings.stepGoal == undefined) { @@ -21,122 +39,227 @@ function loadSettings() { settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } } +loadSettings(); +const showWidgets = settings.showWidgets || false; +let hrtValue; + +// layout values: const colorFg = g.theme.dark ? '#fff' : '#000'; const colorBg = g.theme.dark ? '#000' : '#fff'; const colorGrey = '#808080'; const colorRed = '#ff0000'; -const colorGreen = '#00ff00'; - -let hrtValue; - -const h = g.getHeight(); +const colorGreen = '#008000'; +const colorBlue = '#0000ff'; +const colorYellow = '#ffff00'; +const widgetOffset = showWidgets ? 24 : 0; +const h = g.getHeight() - widgetOffset; const w = g.getWidth(); -const hOffset = 30; +const hOffset = 30 - widgetOffset; const h1 = Math.round(1 * h / 5 - hOffset); const h2 = Math.round(3 * h / 5 - hOffset); -const h3 = Math.round(8 * h / 8 - hOffset); -const w1 = Math.round(w / 6); -const w2 = Math.round(3 * w / 6); -const w3 = Math.round(5 * w / 6); -const radiusOuter = 22; -const radiusInner = 16; +const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position +const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions +const radiusOuter = 25; +const radiusInner = 20; +const circleFont = "Vector:15"; +const circleFontBig = "Vector:16"; +const circleFontSmall = "Vector:13"; function draw() { - g.reset(); + g.clear(true); + + if (!showWidgets) { + /* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ + if (WIDGETS && typeof WIDGETS === "object") { + for (let wd of WIDGETS) { + wd.draw = () => {}; + wd.area = ""; + } + } + } else { + Bangle.drawWidgets(); + } + g.setColor(colorBg); - g.fillRect(0, 0, w, h); + g.fillRect(0, widgetOffset, w, h); // time g.setFont("Vector:50"); - g.setFontAlign(-1, -1); + g.setFontAlign(0, -1); g.setColor(colorFg); - g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8); + g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8); // date & dow - g.setFont("Vector:20"); + g.setFont("Vector:21"); g.setFontAlign(-1, 0); - g.drawString(locale.date(new Date()), w / 10, h2); - g.drawString(locale.dow(new Date()), w / 10, h2 + 22); + g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2); + g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22); - // Steps circle - drawSteps(); - - // Heart circle - drawHeartRate(); - - // Battery circle - drawBattery(); + drawCircle(1); + drawCircle(2); + drawCircle(3); } +const defaultCircleTypes = ["steps", "hr", "battery"]; +function drawCircle(index) { + let type = settings['circle' + index]; + if (!type) type = defaultCircleTypes[index - 1]; + const w = getCirclePosition(type); + switch (type) { + case "steps": + drawSteps(w); + break; + case "stepsDist": + drawStepsDistance(w); + break; + case "hr": + drawHeartRate(w); + break; + case "battery": + drawBattery(w); + break; + case "weather": + drawWeather(w); + break; + } +} -function drawSteps() { +function getCirclePosition(type) { + for (let i = 1; i <= 3; i++) { + const setting = settings['circle' + i]; + if (setting == type) return circlePosX[i - 1]; + } + for (let i = 0; i < defaultCircleTypes.length; i++) { + if (type == defaultCircleTypes[i]) return circlePosX[i]; + } + return undefined; +} + +function isCircleEnabled(type) { + return getCirclePosition(type) != undefined; +} + +function drawSteps(w) { + if (!w) w = getCirclePosition("steps"); const steps = getSteps(); - const blue = '#0000ff'; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w1, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); const stepGoal = settings.stepGoal || 10000; if (stepGoal > 0) { let percent = steps / stepGoal; if (stepGoal < steps) percent = 1; - drawGauge(w1, h3, percent, blue); + drawGauge(w, h3, percent, colorBlue); } g.setColor(colorBg); - g.fillCircle(w1, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFont); g.setFontAlign(0, 0); g.setColor(colorFg); - g.drawString(shortValue(steps), w1 + 2, h3); + g.drawString(shortValue(steps), w + 2, h3); - g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6); + g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6); } -function drawHeartRate() { +function drawStepsDistance(w) { + if (!w) w = getCirclePosition("steps"); + const steps = getSteps(); + const stepDistance = settings.stepLength || 0.8; + const stepsDistance = Math.round(steps * stepDistance); + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w2, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); + + const stepDistanceGoal = settings.stepDistanceGoal || 8000; + if (stepDistanceGoal > 0) { + let percent = stepsDistance / stepDistanceGoal; + if (stepDistanceGoal < stepsDistance) percent = 1; + drawGauge(w, h3, percent, colorGreen); + } + + g.setColor(colorBg); + g.fillCircle(w, h3, radiusInner); + + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); + + g.setFont(circleFont); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(shortValue(stepsDistance), w + 2, h3); + + g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6); +} + +function drawHeartRate(w) { + if (!w) w = getCirclePosition("hr"); + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + + g.setColor(colorGrey); + g.fillCircle(w, h3, radiusOuter); if (hrtValue != undefined && hrtValue > 0) { - const minHR = 40; + const minHR = settings.minHR || 40; const percent = (hrtValue - minHR) / (settings.maxHR - minHR); - drawGauge(w2, h3, percent, colorRed); + drawGauge(w, h3, percent, colorRed); } g.setColor(colorBg); - g.fillCircle(w2, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFontBig); g.setFontAlign(0, 0); g.setColor(colorFg); - g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3); + g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3); - g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6); + g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6); } -function drawBattery() { +function drawBattery(w) { + if (!w) w = getCirclePosition("battery"); const battery = E.getBattery(); - const yellow = '#ffff00'; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.setColor(colorGrey); - g.fillCircle(w3, h3, radiusOuter); + g.fillCircle(w, h3, radiusOuter); if (battery > 0) { const percent = battery / 100; - drawGauge(w3, h3, percent, yellow); + drawGauge(w, h3, percent, colorYellow); } g.setColor(colorBg); - g.fillCircle(w3, h3, radiusInner); + g.fillCircle(w, h3, radiusInner); - g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]); + g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); - g.setFont("Vector:12"); + g.setFont(circleFont); g.setFontAlign(0, 0); let icon = powerIcon; @@ -144,17 +267,95 @@ function drawBattery() { if (Bangle.isCharging()) { color = colorGreen; icon = powerIconGreen; - } - else { + } else { if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { color = colorRed; icon = powerIconRed; } } g.setColor(color); - g.drawString(battery + '%', w3, h3); + g.drawString(battery + '%', w, h3); - g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6); + g.drawImage(icon, w - 6, h3 + radiusOuter - 6); +} + +function drawWeather(w) { + if (!w) w = getCirclePosition("weather"); + const weather = getWeather(); + const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; + const code = weather ? weather.code : -1; + + // Draw rectangle background: + g.setColor(colorBg); + g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + + g.setColor(colorGrey); + g.fillCircle(w, h3, radiusOuter); + + g.setColor(colorBg); + g.fillCircle(w, h3, radiusInner); + + g.fillPoly([w, h3, w - 25, h3 + radiusOuter + 5, w + 25, h3 + radiusOuter + 5]); + + const content = tempString ? tempString : "?"; + g.setFont(content.length < 4 ? circleFont : circleFontSmall); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(content, w, h3); + + if (code > 0) { + const icon = getWeatherIconByCode(code); + if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10); + } +} + +/* + * Choose weather icon to display based on weather conditition code + * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + */ +function getWeatherIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: + return weatherStormy; + case 3: + return weatherCloudy; + case 5: + switch (code) { + case 511: + return weatherSnowy; + case 520: + return weatherPartlyRainy; + case 521: + return weatherPartlyRainy; + case 522: + return weatherPartlyRainy; + case 531: + return weatherPartlyRainy; + default: + return weatherRainy; + } + break; + case 6: + return weatherSnowy; + case 7: + return weatherFoggy; + case 8: + switch (code) { + case 800: + return weatherSunny; + case 801: + return weatherPartlyCloudy; + case 802: + return weatherPartlyCloudy; + default: + return weatherCloudy; + } + break; + default: + return undefined; + } + return undefined; } function radians(a) { @@ -171,11 +372,11 @@ function drawGauge(cx, cy, percent, color) { if (percent > 1) percent = 1; var startrot = -offset; - var endrot = startrot - ((end - offset) * percent) - 15; + var endrot = startrot - ((end - offset) * percent) - 35; g.setColor(color); - const size = 4; + const size = radiusOuter - radiusInner - 2; // draw gauge for (i = startrot; i > endrot - size; i -= size) { x = cx + r * Math.sin(radians(i)); @@ -198,54 +399,56 @@ function shortValue(v) { } function getSteps() { - if (WIDGETS.wpedom !== undefined) { + if (WIDGETS && WIDGETS.wpedom !== undefined) { return WIDGETS.wpedom.getSteps(); } return 0; } -Bangle.on('lock', function(isLocked) { - if (!isLocked) { - Bangle.setHRMPower(1, "watch"); - if (hrtValue == undefined) { - hrtValue = '...'; - drawHeartRate(); - } - } else { - Bangle.setHRMPower(0, "watch"); - } - drawHeartRate(); - drawSteps(); -}); +function getWeather() { + const jsonWeather = storage.readJSON('weather.json'); + return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; +} -Bangle.on('HRM', function(hrm) { - //if(hrm.confidence > 90){ - hrtValue = hrm.bpm; - if (Bangle.isLCDOn()) +function enableHRMSensor() { + Bangle.setHRMPower(1, "circleclock"); + if (hrtValue == undefined) { + hrtValue = '...'; drawHeartRate(); - //} else { - // hrtValue = undefined; - //} -}); - -Bangle.on('charging', function(charging) { - drawBattery(); -}); - -g.clear(); -Bangle.loadWidgets(); -/* - * we are not drawing the widgets as we are taking over the whole screen - * so we will blank out the draw() functions of each widget and change the - * area to the top bar doesn't get cleared. - */ -if (typeof WIDGETS === "object") { - for (let wd of WIDGETS) { - wd.draw = () => {}; - wd.area = ""; } } -loadSettings(); -setInterval(draw, 60000); -draw(); + +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + if (isCircleEnabled("hr")) { + enableHRMSensor(); + } + draw(); + } else { + Bangle.setHRMPower(0, "circleclock"); + } +}); + + +Bangle.on('HRM', function(hrm) { + if (isCircleEnabled("hr")) { + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + } +}); + + Bangle.setUI("clock"); +Bangle.loadWidgets(); + +draw(); +setInterval(draw, 60000); + +Bangle.on('charging', function(charging) { + if (isCircleEnabled("battery")) drawBattery(); +}); + +if (isCircleEnabled("hr")) { + enableHRMSensor(); +} diff --git a/apps/circlesclock/screenshot-dark.png b/apps/circlesclock/screenshot-dark.png new file mode 100644 index 000000000..00c0e3399 Binary files /dev/null and b/apps/circlesclock/screenshot-dark.png differ diff --git a/apps/circlesclock/screenshot-light.png b/apps/circlesclock/screenshot-light.png new file mode 100644 index 000000000..af47b30a4 Binary files /dev/null and b/apps/circlesclock/screenshot-light.png differ diff --git a/apps/circlesclock/screenshot.png b/apps/circlesclock/screenshot.png deleted file mode 100644 index 94ff885fa..000000000 Binary files a/apps/circlesclock/screenshot.png and /dev/null differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index ffda51538..ac4215a8a 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -6,13 +6,26 @@ settings[key] = value; storage.write(SETTINGS_FILE, settings); } + var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"]; + var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"]; E.showMenu({ '': { 'title': 'circlesclock' }, + '< Back': back, + 'min heartrate': { + value: "minHR" in settings ? settings.minHR : 40, + min: 0, + max : 250, + step: 5, + format: x => { + return x; + }, + onchange: x => save('minHR', x), + }, 'max heartrate': { value: "maxHR" in settings ? settings.maxHR : 200, min: 20, max : 250, - step: 10, + step: 5, format: x => { return x; }, @@ -28,7 +41,27 @@ }, onchange: x => save('stepGoal', x), }, - 'battery warn lvl': { + 'step length': { + value: "stepLength" in settings ? settings.stepLength : 0.8, + min: 0.1, + max : 1.5, + step: 0.01, + format: x => { + return x; + }, + onchange: x => save('stepLength', x), + }, + 'step dist goal': { + value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000, + min: 2000, + max : 30000, + step: 1000, + format: x => { + return x; + }, + onchange: x => save('stepDistanceGoal', x), + }, + 'battery warn': { value: "batteryWarn" in settings ? settings.batteryWarn : 30, min: 10, max : 100, @@ -38,6 +71,28 @@ }, onchange: x => save('batteryWarn', x), }, - '< Back': back, + 'show widgets': { + value: "showWidgets" in settings ? settings.showWidgets : false, + format: () => (settings.showWidgets ? 'Yes' : 'No'), + onchange: x => save('showWidgets', x), + }, + 'left': { + value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle1', valuesCircleTypes[x]), + }, + 'middle': { + value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle2', valuesCircleTypes[x]), + }, + 'right': { + value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3, + min: 0, max: 4, + format: v => namesCircleTypes[v], + onchange: x => save('circle3', valuesCircleTypes[x]), + } }); }); diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/limelight/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/limelight/README.md b/apps/limelight/README.md new file mode 100644 index 000000000..49b858127 --- /dev/null +++ b/apps/limelight/README.md @@ -0,0 +1,19 @@ +# Limelight + *Simple configurable analogue clock based on the work of @Andreas_Rozek [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock)* + +![](screenshot_limelight.png) + +* Selection of different fonts +* Settings menu where you can select font, or switch to Vector font and try a range of sizes +* Reduction by 100 lines of code, demonstrating that there is no need for a custom widget draw method +* Full screen option (widgets are loaded but not displayed) + +![](screenshot_gochihand.png) +![](screenshot_monoton.png) +![](screenshot_grenadier.png) + +Many thanks for @Andreas_Rozek for his pioneering work on building an analogue clock toolkit for the Bangle 2. + +Limelight Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) + diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js new file mode 100644 index 000000000..20d79deeb --- /dev/null +++ b/apps/limelight/limelight.app.js @@ -0,0 +1,263 @@ +/* + * Limelight analoguce clock with bolted hands + * Based on the work of @Andreas_Rozek + * [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock) + * + * . Demonstrates simpler approach to establishing the available size of the appRect in relation + * to widgets, avoids having to take on the responsibility for managing the widget draw. + * . Demonstrates a settings menu and various configuration options + * . Demonstrates fullscreen verses, widgets and app area. + * + */ + +g.clear(); + +const SETTINGS_FILE = "limelight.json"; +var UPDATE_PERIOD; +var drawTimeout; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; + settings.secondhand = settings.secondhand||false; + settings.font = settings.font||"Limelight"; + settings.vector = settings.vector||false; + settings.fullscreen = settings.fullscreen||false; + settings.vector_size = settings.vector_size||42; + UPDATE_PERIOD = (settings.secondhand ? 1000 : 60000); +} + +loadSettings(); + +// if we are not full screen then load and draw the widgets so that Bangle.appRect gets set +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +// fonts.google.com +Graphics.prototype.setFontLimelight = function(scale) { + // Actual height 28 (28 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAeAAAAAD8AAAAAf4AAAAB/gAAAAH+AAAAAf4AAAAB/gAAAAD8AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAPwAAAAH8AAAAD+AAAAD+AAAAB/AAAAA/gAAAAfwAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAA/gAAAA//wAAAP//wAAB///wAAP///gAA///+AAH///8AAf///4AD////gAP///+AA////4AD////gAMAAAGAAwAAAYADAAABgAMAAAGAAwAAAwABgAADAAHAAAYAAOAADgAAeAA8AAAfh/AAAAf/wAAAAHgAAAAAAAAAAGAAAAAAYAAAAABAAAAAAMAAAAAAwAAAAAD///+AAf///4AB////gAH///+AAf///4AD////gAP///+AA////4AH////gAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgABAAAeAAOAAH4AAwAA/gAGAAP+AAYAB/4ADAAf/gAMAD/+AAwAf/4ADAH//gAMA//+AAwH//4ADB//9gAOP//GAA///wYAD//+BgAH//gGAAf/8AYAA//ABgAB/4AGAAD+AAYAADAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAHAAAwAAGAAHAAAMAAYAAAwADAEABgAMAwAGAAwDAAYADAMABgAMAwAGAAwDAAYAD////gAP///+AA////4AD////gAP///8AAf///wAB/7//AAD/H/4AAP4f/AAAPA/4AAAAA+AAAAAAAAAAAAAAAAAAAGAAAAAB8AAAAAPwAAAADzAAAAAcMAAAAHgwAAAA8DAAAAHAMAAAB4AwAAAOADAAADwAMAAAcAAwAAD///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD////gAP///+AAAAAMAAAAAAwAAAAADAAAAAAcAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAHAAAH4AOAAP/gAcAA+GAAwADAwABgAMDAAGAAwMAAYADAwABgAMDAAGAAwMAAYADA///gAMD//+AAwP//4ADA///AAMB//8AAwH//wADAP/+AAIAf/wAAAA/+AAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAD/8AAAA//8AAAH//4AAB///wAAH///gAA////AAH///8AAf///4AD////gAP///+AAwAAAYADAYABgAMBgAGAAwGAAYADAYABgAMBgAGAAwGAAwABgYADAAHAwAYAAMDgHAAAAHh4AAAAP/AAAAAHgAAAAAAAAAAAAAAAA+AAAAAD4AAAAAMAAAAAAwAAAAADAAAAAAMAA/+AAwB//4ADB///gAM///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD//+AAAP/wAAAA/wAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAA/g/wAAH/GDgAA/+wGAAD//AMAAf/4AwAB//gBgAP//AGAAx/+AYADD/4BgAMH/wGAAwP/AYACA/+BgAMB/8GAAwD/wYADAP/jgAMBf/+AAYH//wABgz//AADHH/4AAH4f/gAAOA/8AAAAB/AAAAAAwAAAAAAAAAAAAAAAAAeAAAAAH/AAAAB8eAAAAGAcBgAAwAwHAAGABgMAAYAGAYADAAIBgAMAAwGAAwADAYADAAYBgAMAAgGAAwAAAYAD////gAP///+AA////wAB////AAH///4AAP///AAA///8AAA///AAAB//4AAAB/+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAeAAAD8D8AAAf4f4AAB/h/gAAH+H+AAAf4f4AAB/h/gAAD8D8AAAHgHgAAAAAAAAAAAAAAA="), 46, atob("DQ0aExgZHRkbGBsbDQ=="), 40+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontGochiHand = function(scale) { + // Actual height 29 (31 - 3) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAB4AAAAAD4AAAAAB4AAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAH/gAAAD//gAAD///gAD///+AAH///AAAH//gAAAH/wAAAAHwAAAAAAAAwAAAAAP+AAAAA//gAAAB//wAAAD//4AAAD8P4AAAHwD8AAAHgB8AAAPgA8AAAPAA8AAAPAA8AAAPAA8AAAPgA8AAAPgA8AAAPgB8AAAHwB4AAAH4D4AAAD+PwAAAD//gAAAB//gAAAA/+AAAAAP8AAAAAAAAAAAAAAAAAAAcAAAAAA8AAAAAB8AAAAAD4AAAAAD4AAAAAHwAAAAAHgAAAAAPgAAAAAPgAAAAAf/AAAAAf//wAAAP//wAAAH//wAAAAf/wAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAA8AAAeAB8AAA+AD8AAB+AH8AAB4AP8AAD4AP8AADwAf8AADwB+8AAD4D88AAD4H48AAD//w+AAB//g+AAB//A+AAA/8A+AAAPwA+AAAAAA+AAAAAAcAAAAAAIAAAAAAAAAA8AAAAAB8AAAAAB8AAAAAB4AHgAAD4AHwAAD4AH4AADwPH8AADwfB8AADwfA8AAD4fA+AAD4fA+AAB//A+AAB//A+AAA//A8AAA//x8AAAPP/8AAAAH/4AAAAD/wAAAAB/gAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAH8AAAAAP+AAAAA/+AAAAB/+AAAAD8+AAAAP4+AAAB/weAAAD/weAAAD/8eAAAB///AAAA///AAAAH//8AAAAf//AAAAD//AAAAAf/AAAAAf+AAAAAfAAAAAAMAAAAAAAAAAAAAAAAAAPA/gAAAfh/wAAA/x/4AAA/x/4AAB/4/8AAB74B8AAB58A8AAB58A+AAB5+A+AAB4+A+AAB4+A+AAB4fA+AAB4fg+AAB4Pg8AAB4P58AAB4H/4AAB4D/4AAB4D/wAAAQA/gAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAB/+AAAAD//gAAAH//gAAAPwfwAAAPgf4AAAfAf4AAAeA/8AAAeA98AAAeA88AAAfB48AAAfB48AAAPB48AAAOB48AAAAB98AAAAB/8AAAAA/4AAAAA/wAAAAAfgAAA8AAAAAA8BAAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA+HgAAAA+HgAAAA+HgAAAAfHgAAAAf//+AAAf//+AAAP//+AAAH//8AAAAPwAAAAAHgAAAAAHwAAAAAHwAAAAAHwAAAAADwAAAAADgAAAAAAAAAAAAAAAAAAAB/AAAAP3/wAAAf//wAAA///4AAA//D8AAB9+B8AAB4+A8AAB4+A+AAB4+A+AAB4+A+AAB8+A+AAB8+A+AAA/+A8AAA//A8AAAf/x8AAAP//4AAAH//wAAAAD/gAAAAB/AAAAAAAAAAAAAAAAAAD/AAAAAD/gAAAAH/gAAAAP/wAAAAPHwAAAAPDwAAAAeDwAAAAeDwAAAAeDwAAAAeHwAAAAeHgAAAAePgAAAAefAAAAAf/AAAAAf///gAAf///wAAP///wAAP///gAAH8AAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BwAAAA8B4AAAA+D4AAAA8B4AAAAcB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), 46, atob("DQoYERUWFBYVFhcVDQ=="), 42+(scale<<8)+(1<<16)); +} + +// free for commercial use +// https://www.1001fonts.com/search.html?search=Grenadier+NF +Graphics.prototype.setFontGrenadierNF = function(scale) { + // Actual height 39 (39 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAEAAAAAAPgAAAAA/8AAAAB//gAAAD//4AAAP//wAAAf//AAAB//+AAAD//8AAAB//wAAAAP/gAAAAB+AAAAAAMAAAAAAAAAAAAAAAAB4AAAAAD/8AAAAB//4AAAA///wAAAP8D/AAAD8AD8AAA/AAPwAAPgAAfAAD4AAB8AAeAAAHgAHwAAA+AA8AAADwAHgAAAeAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APgAAAfAA8AAADwAHwAAA+AAeAAAHgAD4AAB8AAPgAAfAAA+AAHwAAD8AD8AAAP8D/AAAA///wAAAD//8AAAAH/+AAAAAD8AAAAAAAAAAAAAAAAAAABAAAAAAAcAAAAAAHwAAAAAB8AAAAAAfgAAAAAH/////AB/////4AP/////AB/////4AAAAAAAAAAAAAAAAAAAAADAAAAAAA4APAAAAPAB4AAAD4APAAAA/AB4AAAP4APAAAD/AB8AAA/4AHgAAPvAA8AAD54ADwAB+PAAfAAfh4AB8AH4PAAPwD+B4AA///gPAAD//wB4AAH/4APAAAP8AAAAAAAAAAAAAAAAAAAPAAAAHAB4AAAB4APAAAAPAB4PAAB4APB4AAPAB+/gAB4AH/8AAfAAf/wADwAB/fAA+AABD8APgAAAPwH4AAAA//+AAAAD//gAAAAH/4AAAAAP8AAAAAAAAAAAAAAAAAAAAAAYAAAAAAPAAAAAAH4AAAAAD/AAAAAB/4AAAAA//AAAAAf94AAAAH+PAAAAD/B4AAAB/gPAAAA/wB4AAAf8APAAAP////4AH/////AD/////4AAAAAPAAAAAAA4AAAAAAAAAAAAAAAAAAAIAAPAAAPAAB4AAf4AAPAA//gAB4AP/8AAPAB/3gAB4APg8AAfAB4DwADwAPAfAA+AB4B8APgAPAP4H4AB4A//+AAPAB//gAB4AH/4AAAAAP8AAAAAAAAAAAAAB4AAAAAD/4AAAAA//wAAAAf//AAAAP+H8AAAH+AHwAAB/gAfAAA/4AB4AAf+AAPAAP/wAA8AH+eAAHgB/ngAA8APw8AAHgB4HgAA8AMA8AAHgAADwAA8AAAeAAPgAAD4AB4AAAPgAfAAAA+AHwAAAH4D8AAAAf//AAAAB//wAAAAD/8AAAAAH8AAAAAAAAAAAAAAAAAAAAAAAYAAAAAAfAB4AAAP4APAAAH/AB4AAH/gAPAAD/wAB4AD/wAAPAB/4AAB4A/8AAAPA/8AAAB4f+AAAAPP+AAAAB//AAAAAP/gAAAAB/gAAAAAPwAAAAAB4AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAB//AAAAAf/+AAAAH//4AAAB+AfgAA+fAB8AAP/wAHwAD/+AAeAA//gAB4APj8AAPAB4PAAB4APB4AAPAB4PAAB4APB4AAPAB8fgAB4AH/8AAPAAf/wADwAB/eAA+AADj4APgAAAPwD8AAAA///AAAAD//wAAAAP/4AAAAAf8AAAAAAAAAAAB4AAAAAB/4AAAAA//wAAAAP//AAAAD+H8AAAA/AHwAAAHgAfAAAB8AB4AAAPAAHgAADwAA8AAAeAAHgBADwAAeA4AeAADwfADwAAeP4AeAAD3+ADwAA9/gAfAAH/wAB4AB/4AAPgAf8AAA+AH+AAAD8B/gAAAP//wAAAA//4AAAAD/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAeB4AAAADwPAAAAAeB4AAAAAAAAAAAAAAAA=="), 46, atob("Bg4kChURExEaFBoaBg=="), 45+(scale<<8)+(1<<16)); +} + +// fonts.google.com +Graphics.prototype.setFontMonoton = function(scale) { + // Actual height 38 (37 - 0) + g.setFontCustom(atob("AAAAAAAAAAEkAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAASQAAAAAAAAAAAAAAAAAAAB4AAAAB/gAAAB/gAAAB/h4AAB/h/AAB/h/CAB/h/D4B/h/D/B/j/D/APj/D/AAD/D/AAB/D/AAAPD/AAAAD/AAAAB/AAAAAPAAAAAAAAAAAAAAAAAAAAAD/wAAAB//4AAAfAD4AAHj/x4AA5//5wAHfAB5gAzj/5zAHc//5mAbvDBzcDdz/zmwNu+HzZhs3ADu2G2YAHbYbbAANthtsAA2yG2wABtsbbAAG2xtsAA2yG2wADbYbZgAdthm3ADs2DZv/92wM3P/O7AbvAD3YB3P/87gDvP/HMAHPgD7gAOP/+cAAfH+HgAAfgH4AAAf/+AAAAD+AAAAAAAAAAAAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wEn///+AAAAAAAAAAAAAADQAAAUgdoAAF7BtsAA3sG2wAOewbbAB17BtsAc3sG2wDnewbbAdx7BtsHO3sG2w7newbbPd57Btv3OXsGzc73ewZuPc57A2f3nHsDMc5wewG8POB7Ac/zgHsA4Y8AewB+fABSAB/wAAAAAAAAAAAAAAAAA0AAAAsHbAAAbYbbAANthtsAA22G2wADbYbbJJNthts22W2G2zbZpIbbNtm2xts22bbG2zbZJIbbNttthtv2322Gzfbu7YNuO3HZg3f7P5sDuf2OMwGeHPHmAO/2f8wAccOOOAA/PfvwAA/4f8AAAAAAAAAAAAAAAAABkgAAAA/bAAAAPtsAAAD52wAAA8fbAAAfH9sAAHz42wAB8+fbAAePn9sADjx82wAJ8ePbAAfPj9sADz482wAI+fDbAAfHwNsADx8A2wAM+D/b+APgP9v4D4AA2wAMAD/b+AAAP9v4AAAA2wAAAADSAAAAAAAAAAAAAAAAAAAAMAaf/8AwBt//wJgG2AAA3Abf/8JsBt//w2wG2AABtgbf/822BtgADbYG2NvNtgbY28SSBtjbxtsG2NvG2gbY28SSBtjbzbYG2Nv9tgbY23m2BtjNg2YG2Gz+bAbYZnzcBtgzg5gG2Dn/MASQHHzgAAAPg8AAAAP/gAAAAHwAAAAAAAAAAAAAAAAA//4AAAf//8AAHgAB4AA4//44AGf//5wAzgAB7AGY//52AbP//7MDZwABmwNu//zZhs3//m2G25LTbYbbN7Nthts3sSSG2zexpIbbN7G2xts3sSaG2zezbYbbN7Nthts3v22GbDbezYNsG+HbA3Qbf7sBsB3edgGQDPHuAMAGf9wAQAOOOAAAAfvwAAAAf8AAAAAAAAAAAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAAbYAAAMBtgAAPwG2AAP8AbYAf4cBtgf4fwG0f4f4Aaf4f4cAf4f4fwH4f4f4AYf4f4cA/4/w/wHw/w/wAQ/w/wAA/w/wAAHw/wAAAQ/wAAAA/wAAAAHwAAAAAAAAAAAAAAAAAAAA+AfAAAf/P/gADwPwHgA5/OfnAHP+f/OAZwO4HYDM+d/MwNn+3/bBuwZsM2G2e2+bYbb7b9thtskk2yG2zbZtobbNtm2xts22bbG2zbZtsbbNtm2xts22bbG2zbZNsbbNttshtv2322GzfZu7YNuO3HZg3f7v5sBud3OcwHfPvHmAOf3P8wAcAeAOAAf///wAA/4f8AAAAAAAAAAAAAAAAfwAAAAH/wAAAA4DwAIAGfzgAwAz/3AJgGYDsA2AzP2YDsDZz9g2wNszbDNhs3ns22G2zezbYbbN5NthtsTkSSG2xORtMbbE5G2xts3sySG2zezbYbbN7Nths2ABm2Cbf/+3YNmf/nbAzeAB7MBuf/+dgHcP/DuAO///9wAc///OAA8AADwAA///8AAA///AAAAAAAAAAAAAAAAAAAAAAA2xtgAADbG2AAANsbYAAA2xtgAADbG2AAANsbYAAAkhJAAAAAAAAAAAAAAAA"), 46, atob("ChIiERcYGRwfGSAfCw=="), 40+(scale<<8)+(1<<16)); +} + +/* + * If only 1 widget is loaded at the top, then Bangle.appRect changes + * to report as if widgets were loaded at the bottom as well. The + * other option would be for Bangle.appRect to adjust for different + * combinations EG: no widgets, wigets on top, widgets on bottom and + * widgets on top and bottom areas, but it does not at present. + * + * Example of Bangle.appRect with 3 widges on the top, note h = 152, not 176 + * ={ x: 0, y: 24, w: 176, h: 152, x2: 175, y2: 175 } + * + * With the example below we are going assume that the bottom widget + * space is not used. + * + */ +const CenterX = g.getWidth()/2; +const CenterY = (g.getHeight()/2) + (Bangle.appRect.y/2); +const outerRadius = (g.getHeight() - Bangle.appRect.y)/2; + +if (settings.fullscreen) { + Bangle.loadWidgets(); + /* + * We load the widgets as some like widpedom accumualte the step count. + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * widgets area to the top bar doesn't get cleared. + */ + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +function debug(o) { + //console.log(o); +} + +debug("limelight.app.js"); +debug("CenterX=" + CenterX); +debug("CenterY=" + CenterY); +debug("outerRadius=" + outerRadius); +debug("y12=" + (CenterY - outerRadius)); +debug("y6=" + (CenterY + outerRadius)); + +let HourHandLength = outerRadius * 0.5; +let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2; + +let MinuteHandLength = outerRadius * 0.7; +let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2; + +let SecondHandLength = outerRadius * 0.9; +let SecondHandOffset = halfHourHandWidth + 10; + +let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4; +let HandOffset = outerBoltRadius + 4; + +let twoPi = 2*Math.PI, deg2rad = Math.PI/180; +let Pi = Math.PI; +let halfPi = Math.PI/2; + +let sin = Math.sin, cos = Math.cos; + +let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1]; + +let HandPolygon = [ + -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3], + sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0], + sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3], + -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0], +]; + +let HourHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i]; + HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1]; + if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; } + if (i > l/2) { HourHandPolygon[i+1] += HandOffset; } +} +let MinuteHandPolygon = new Array(HandPolygon.length); +for (let i = 0, l = HandPolygon.length; i < l; i+=2) { + MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i]; + MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1]; + if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; } + if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; } +} + +/**** transforme polygon ****/ + +let transformedPolygon = new Array(HandPolygon.length); + +function transformPolygon (originalPolygon, OriginX,OriginY, Phi) { + let sPhi = sin(Phi), cPhi = cos(Phi), x,y; + + for (let i = 0, l = originalPolygon.length; i < l; i+=2) { + x = originalPolygon[i]; + y = originalPolygon[i+1]; + + transformedPolygon[i] = OriginX + x*cPhi + y*sPhi; + transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi; + } +} + +/**** draw clock hands ****/ + +function drawClockHands () { + let now = new Date(); + + let Hours = now.getHours() % 12; + let Minutes = now.getMinutes(); + let Seconds = now.getSeconds(); + + let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; + let MinutesAngle = (Minutes/60) * twoPi - Pi; + let SecondsAngle = (Seconds/60) * twoPi - Pi; + + g.setColor(g.theme.fg); + + transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle); + g.fillPoly(transformedPolygon); + + transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle); + g.fillPoly(transformedPolygon); + + let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle); + + if (settings.secondhand) { + g.setColor(g.theme.fg2); + g.drawLine( + CenterX + SecondHandOffset*sPhi, + CenterY - SecondHandOffset*cPhi, + CenterX - SecondHandLength*sPhi, + CenterY + SecondHandLength*cPhi + ); + } + + g.setColor(g.theme.fg); + g.fillCircle(CenterX,CenterY, outerBoltRadius); + + g.setColor(g.theme.bg); + g.drawCircle(CenterX,CenterY, outerBoltRadius); + g.fillCircle(CenterX,CenterY, innerBoltRadius); +} + +function setNumbersFont() { + if (settings.vector) { + g.setFont('Vector', settings.vector_size); + return; + } + + if (settings.font == "GochiHand") + g.setFontGochiHand(); + else if (settings.font == "Grenadier") + g.setFontGrenadierNF(); + else if (settings.font == "Monoton") + g.setFontMonoton(); + else + g.setFontLimelight(); +} + +function drawNumbers() { + g.setColor(g.theme.fg); + setNumbersFont(); + + g.setFontAlign(0,-1); + g.drawString('12', CenterX, CenterY - outerRadius); + + g.setFontAlign(1,0); + g.drawString('3', CenterX + outerRadius, CenterY); + + g.setFontAlign(0,1); + g.drawString('6', CenterX, CenterY + outerRadius); + + g.setFontAlign(-1,0); + g.drawString('9', CenterX - outerRadius,CenterY); +} + +function draw() { + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + drawClockHands(); + drawNumbers(); + queueDraw(); +} + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, UPDATE_PERIOD - (Date.now() % UPDATE_PERIOD)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI('clock'); +draw(); diff --git a/apps/limelight/limelight.icon.js b/apps/limelight/limelight.icon.js new file mode 100644 index 000000000..f7e74db90 --- /dev/null +++ b/apps/limelight/limelight.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lksgIqngf/wAFC//+AgUch/4AgMBwAQEh/8Dgf/4AKOEAQKCAYUB//gAoU/DQkPBQYVBGx5SDBQIbDBR0GEAlgFYcHGwh4B+CDHRwL04")) \ No newline at end of file diff --git a/apps/limelight/limelight.png b/apps/limelight/limelight.png new file mode 100644 index 000000000..b1744b28e Binary files /dev/null and b/apps/limelight/limelight.png differ diff --git a/apps/limelight/limelight.settings.js b/apps/limelight/limelight.settings.js new file mode 100644 index 000000000..aacea2f86 --- /dev/null +++ b/apps/limelight/limelight.settings.js @@ -0,0 +1,78 @@ +(function(back) { + const SETTINGS_FILE = "limelight.json"; + + // initialize with default settings... + let s = { + 'vector_size': 42, + 'vector': false, + 'font': "Limelight", + 'secondhand': false, + 'fullscreen': false + } + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || {} + const saved = settings || {} + + // copy settings into variable + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var font_options = ["Limelight","GochiHand","Grenadier","Monoton"]; + + E.showMenu({ + '': { 'title': 'Limelight Clock' }, + '< Back': back, + 'Full Screen': { + value: s.fullscreen, + format: () => (s.fullscreen ? 'Yes' : 'No'), + onchange: () => { + s.fullscreen = !s.fullscreen; + save(); + }, + }, + 'Font': { + value: 0 | font_options.indexOf(s.font), + min: 0, max: 3, + format: v => font_options[v], + onchange: v => { + s.font = font_options[v]; + save(); + }, + }, + 'Vector Font': { + value: s.vector, + format: () => (s.vector ? 'Yes' : 'No'), + onchange: () => { + s.vector = !s.vector; + save(); + }, + }, + 'Vector Size': { + value: s.vector_size, + min: 24, + max: 56, + step: 6, + onchange: v => { + s.vector_size = v; + save(); + } + }, + 'Second Hand': { + value: s.secondhand, + format: () => (s.secondhand ? 'Yes' : 'No'), + onchange: () => { + s.secondhand = !s.secondhand; + save(); + }, + } + }); +}) diff --git a/apps/limelight/screenshot_gochihand.png b/apps/limelight/screenshot_gochihand.png new file mode 100644 index 000000000..244b008dc Binary files /dev/null and b/apps/limelight/screenshot_gochihand.png differ diff --git a/apps/limelight/screenshot_grenadier.png b/apps/limelight/screenshot_grenadier.png new file mode 100644 index 000000000..a55896297 Binary files /dev/null and b/apps/limelight/screenshot_grenadier.png differ diff --git a/apps/limelight/screenshot_limelight.png b/apps/limelight/screenshot_limelight.png new file mode 100644 index 000000000..7b12e4cc2 Binary files /dev/null and b/apps/limelight/screenshot_limelight.png differ diff --git a/apps/limelight/screenshot_monoton.png b/apps/limelight/screenshot_monoton.png new file mode 100644 index 000000000..e75b11f5d Binary files /dev/null and b/apps/limelight/screenshot_monoton.png differ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 448f8119a..39b825e02 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -14,3 +14,5 @@ 0.12: Fixed nl_NL formatting, because the full months won't fit on the Bangle.js2's screen 0.13: Now use shorter de_DE date format to more closely match other languages for size 0.14: Added some first translations for Messages in nl_NL +0.15: Fixed sv_SE formatting, long date does not work well for Bangle.js2 + Added Swedish localisation with English text \ No newline at end of file diff --git a/apps/locale/locales.js b/apps/locale/locales.js index cf511c54f..428e0c773 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -276,13 +276,31 @@ var locales = { temperature: "°C", ampm: { 0: "fm", 1: "em" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", "1": "%Y-%m-%d" }, // söndag 1 mars 2020 // 2020-03-01 + datePattern: { 0: "%b %d %Y", "1": "%Y-%m-%d" }, // feb 1 2020 // 2020-03-01 abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec", month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december", abday: "sön,mån,tis,ons,tors,fre,lör", day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" } }, + "en_SE": { // Swedish localisation with English text + lang: "en_SE", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "kr", + int_curr_symbol: "SKR", + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%B %d %Y", "1": "%Y-%m-%d" }, // March 1 2020 // 2020-03-01 + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", + // No translation for english... + }, "en_NZ": { lang: "en_NZ", decimal_point: ".", diff --git a/apps/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog index defdb5049..050cf2049 100644 --- a/apps/menuwheel/ChangeLog +++ b/apps/menuwheel/ChangeLog @@ -1 +1,2 @@ 0.01: New menu! +0.02: Clean up touch handler in setUI diff --git a/apps/menuwheel/boot.js b/apps/menuwheel/boot.js index 3e708e9a8..deb15264d 100644 --- a/apps/menuwheel/boot.js +++ b/apps/menuwheel/boot.js @@ -1,8 +1,5 @@ E.showMenu = function(items) { g.clearRect(Bangle.appRect); // clear screen if no menu supplied - // clean up back button listener - if (Bangle.backHandler) Bangle.removeListener('touch', Bangle.backHandler) - delete Bangle.backHandler; if (!items) { Bangle.setUI(); return; @@ -206,8 +203,13 @@ E.showMenu = function(items) { if (b===1) back(); } } - // note: backHandler is cleaned up at the top of this file Bangle.on('touch', Bangle.backHandler); } return l; }; +// setUI now also needs to clear up our back button touch handler +Bangle.setUI = (old => function() { + if (Bangle.backHandler) Bangle.removeListener("touch", Bangle.backHandler); + delete Bangle.backHandler; + return old.apply(this, arguments); +})(Bangle.setUI); \ No newline at end of file diff --git a/apps/messages/app.js b/apps/messages/app.js index e36bb699e..6e51c2b33 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -83,7 +83,7 @@ function getMessageImage(msg) { if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); if (s=="facebook") return getFBIcon(); if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); - if (s=="instagram") return atob("GBiBAf////////////////wAP/n/n/P/z/f/b/eB7/c87/d+7/d+7/d+7/d+7/c87/eB7/f/7/P/z/n/n/wAP////////////////w=="); + if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); if (s=="gmail") return getNotificationImage(); if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); if (s=="mail") return getNotificationImage(); diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog new file mode 100644 index 000000000..939ac3b5d --- /dev/null +++ b/apps/mmind/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mmind/README.md b/apps/mmind/README.md new file mode 100644 index 000000000..8060b95f6 --- /dev/null +++ b/apps/mmind/README.md @@ -0,0 +1,31 @@ +# Mastermind + +Play the classic mind game mastermind on your Bangle 2. + +![](screenshot_mmind.png) + + +## Game +The game will start when run. +Four colors pins are randomly chosen and kept secret. +You need to find the secret by scoring your choice within 6 turns. +The game makes use of touch features. + + +## Play +Select one of the dots, the color menu will show, select a colour for the pin. +If all pins are chosen with a color the red button will turn green. +Hit the green button and your play will be scored and listed from the top. +The first digit shows the number of pins with the correct color and in the right place. +The second digit gives the number of pins with the correct color but in the wrong place. +There are six turns to get the correct secret. +The blue button will start a new game. + + +## Requests +This is the first version, things to add are: +Add a menu to change game options like the number of colors, allow double colors, 5 pins per row. Add feature to drag screen up and down to see more scores. Timer and high score. +Any other fearures or remarks, let me know @psbest. + +## Creator +This game is created by Peter Slendebroek. diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js new file mode 100644 index 000000000..e7def025d --- /dev/null +++ b/apps/mmind/mmind.app.js @@ -0,0 +1,198 @@ +//MMind + +//set vars +const H = g.getWidth(); +const W = g.getHeight(); +var touch_actions = []; +var cols = ["#FF0000","#00FF00","#0000FF", "#FF00FF", "#FFFF00", "#00FFFF", "#000000","#FFFFFF"]; +var turn = 0; +var col_menu = false; +//pinsRow = 6; +//pinsThick = 10; +//pinsRow = 5; +//pinsThick = 10; +var pinsRow = 4; +var pinsThick = 10; +var play = [-1, -1, -1, -1]; + +var pinsCol = 5; +var playx = -1; +var sx = (W - 30 )/pinsRow; +var sy = (H - 20 )/7; +var touch_actions = []; +var secret = []; +var secret_no_dub = true; +var endgame = false; + +g.clear(); +g.setColor("#FFFFFF"); +g.fillRect(0, 0, H, W); +g.setFont("Vector12",45); + +function draw() { + touch_actions = []; + g.clear(); + g.setColor("#FFFFFF"); + g.fillRect(0, 0, H, W); + g.setColor("#000000"); + //draw scores + for (y=0;y= 0) s = Math.round(Math.random()*pinsCol); + secret[i]= s; + } + } + +function score() { + bScore = 0; + wScore = 0; + for (i=0; i touch_actions[i][0][0] && e.x < touch_actions[i][0][2] && + e.y > touch_actions[i][0][1] && e.y < touch_actions[i][0][3]) { + // a action is hit, add acctions here, todo: start, stop, new, etc. + switch (touch_actions[i][1][0]) { + case 1: + //get pins col menu + col_menu = 1; + playx = touch_actions[i][1][1]; + break; + case 2: + //copy choice col to play + play[playx] = touch_actions[i][1][1]; + col_menu = 0; + break; + case 3: + //score play + var sc; + sc = score(); + game.push([play, sc]); + play = [-1,-1,-1,-1]; + turn+=1; + if (turn==6 || sc[0]==pinsRow) { + play = secret; + col_menu = 0; + endgame = true; + } + break; + case 4: + //new game + play = [-1,-1,-1,-1]; + game = []; + endgame=false; + break; + } + } + } + //console.log(touch_actions[i][1][0], touch_actions[i][1][1]); + + draw(); + } +); + + +game = []; +get_secret(); +draw(); +//Bangle.loadWidgets(); +//Bangle.drawWidgets(); + + + + + diff --git a/apps/mmind/mmind.icon.js b/apps/mmind/mmind.icon.js new file mode 100644 index 000000000..17c28ba0f --- /dev/null +++ b/apps/mmind/mmind.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+64A/AEOBq2sBAusqwJHCaQFDAYlP2m0yGBCIkSj0eiWHBIkDgsFgYTE01v3O5t4mC1krgAEBq0ACYQuCAANsHIcxFwIwCEocsFwIwCBIYuCAANQF4QwBOgQABAgNIF4ZgELwQvCHIcCF4cEKwYvEt45DF4QwCL5YvFL5ITDF6OstheCvTjEjAuBjDJFX4UEq4TEyguBygTEF4dWBIeskkkqwQDDgUGgwaEBIUBgITHkslCYeBd4MrqwDBAgIuBcwRVGNIVs0oJEv3S6V+CYmIisjkcVZAYpBgDyBAAJFBFwTlGZIolDqouBGAQJDFwQABmRfCFAICCGwXXhgvDMAheCfI1UF4eoKwYvEiovHSoJfLF4pfJCYYvN1gwBAYMSLwVcbQmQFwOQZIq/C1GACYkcFwMcCYQoCLYNWF4KPBDgNWmIkEBIVPp5TDBIdWqoTHmUyCYlWRQTwCD4wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHmy2QJH6PRBI/Q6AkOCAIAFBINDjwABGInR3+53O/GIu72gABGJnQCAQAE69oFwQABCYfFFwIwCBIfCDIe7FIus1gvXLwQACLw4aCAAkAgAvcL4gvLq1WF5uyFwdoCYfLF4fLDpHCX6owBtFoxoUF6PF4ruFDwPC4XJFxbSCAAwVNAH4ARA")) diff --git a/apps/mmind/mmind.info b/apps/mmind/mmind.info new file mode 100644 index 000000000..2e79822b1 --- /dev/null +++ b/apps/mmind/mmind.info @@ -0,0 +1,17 @@ + { + "id": "mmind", + "name": "Classic Mind Game", + "shortName":"Master Mind", + "icon": "mmind.png", + "version":"0.01", + "description": "This is the classic game for masterminds", + "type": "game", + "tags": "mastermind, game, classic", + "readme":"README.md", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"mmind.app.js","url":"mmind.app.js"}, + {"name":"mmind.img","url":"mmind.icon.js","evaluate":true} + ] + } diff --git a/apps/mmind/mmind.png b/apps/mmind/mmind.png new file mode 100644 index 000000000..14a3ef7c6 Binary files /dev/null and b/apps/mmind/mmind.png differ diff --git a/apps/mmind/screenshot_mmind.png b/apps/mmind/screenshot_mmind.png new file mode 100644 index 000000000..5c886e7e8 Binary files /dev/null and b/apps/mmind/screenshot_mmind.png differ diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index 627531f03..00090fcd1 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -8,3 +8,4 @@ 0.08: Added dependancy on MyLocation 0.09: Added dependancy on Pedometer Widget 0.10: Added Weather line, fixed issues on a Bangle 1, update every minute +0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index db60a2738..3e64cdd9c 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -255,7 +255,7 @@ function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; - nextInfo(); + prevInfo(); draw(); }, 60000 - (Date.now() % 60000)); } diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog index fb6c751bb..b9be6039d 100644 --- a/apps/qalarm/ChangeLog +++ b/apps/qalarm/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fix unfreed memory, and clearInterval that disabled all clocks at midnight Fix app icon Change menu order so 'back' is at the top +0.04: Fix alarm not activating sometimes. diff --git a/apps/qalarm/qalarm.js b/apps/qalarm/qalarm.js index 6b31ba645..8e82be186 100644 --- a/apps/qalarm/qalarm.js +++ b/apps/qalarm/qalarm.js @@ -143,7 +143,7 @@ let alarms = require("Storage").readJSON("qalarm.json", 1) || []; let active = alarms.filter( (alarm) => alarm.on && - alarm.t < t && + alarm.t <= t && alarm.last != time.getDate() && (alarm.timer || alarm.daysOfWeek[time.getDay()]) ); diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 4bdf7f304..27ce24e50 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -154,8 +154,8 @@ function showAlertsMenu() { function showBLEMenu() { - var hidV = [false, "kbmedia", "kb", "joy"]; - var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; + var hidV = [false, "kbmedia", "kb", "com", "joy"]; + var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"]; E.showMenu({ '': { 'title': 'Bluetooth' }, '< Back': ()=>showMainMenu(), diff --git a/apps/simplest/ChangeLog b/apps/simplest/ChangeLog index f37015d6a..e7ab5f2c3 100644 --- a/apps/simplest/ChangeLog +++ b/apps/simplest/ChangeLog @@ -1,3 +1,6 @@ 0.01: Modified for use with new bootloader and firmware 0.02: Use Bangle.setUI for button/launcher handling 0.03: Fix display for Bangle 2 +0.04: Use queueDraw(), update every minute, respect theme, use Lato font +0.05: Decided against custom font as it inceases the code size + minimalism is useful when narrowing down issues diff --git a/apps/simplest/app.js b/apps/simplest/app.js index 68564ff33..582c4c2d5 100644 --- a/apps/simplest/app.js +++ b/apps/simplest/app.js @@ -1,28 +1,55 @@ - const h = g.getHeight(); const w = g.getWidth(); function draw() { - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4].substr(0,5); - + var date = new Date(); + var timeStr = require("locale").time(date,1); + g.reset(); - g.clearRect(0, 30, w, 99); - g.setFontAlign(0, -1); - g.setFont("Vector", w/3); - g.drawString(time, w/2, 40); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + g.setFont('Vector', w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + + queueDraw(); } -// handle switch display on by pressing BTN1 -Bangle.on('lcdPower', function(on) { - if (on) draw(); +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } }); g.clear(); + +// Show launcher when middle button pressed +//Bangle.setUI("clock"); +// use clockupdown as it tests for issue #1249 +Bangle.setUI("clockupdown", btn=> { + draw(); +}); + + +// Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); -setInterval(draw, 15000); // refresh every 15s draw(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/widhwt/ChangeLog b/apps/widhwt/ChangeLog index 4c21f3ace..76c4fcec9 100644 --- a/apps/widhwt/ChangeLog +++ b/apps/widhwt/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Ported to Bangle.js2 diff --git a/apps/widhwt/app.js b/apps/widhwt/app.js new file mode 100644 index 000000000..f18e78643 --- /dev/null +++ b/apps/widhwt/app.js @@ -0,0 +1,13 @@ +// Replace the "Loading..." box +// with our own message +g.clearRect(38, 68, 138, 108); +g.drawRect(38, 68, 138, 108); +g.setFontVector(13); +g.setFontAlign(0, 0, 0); +g.drawString("Wash...", g.getWidth()/2, g.getHeight()/2); + +Bangle.buzz(); +setTimeout(() => { + Bangle.buzz(1E3, 1); + setTimeout(() => load(), 2E3); +}, 35E3); diff --git a/apps/widhwt/widget.js b/apps/widhwt/widget.js index d178a5b5d..5e1f95a41 100644 --- a/apps/widhwt/widget.js +++ b/apps/widhwt/widget.js @@ -6,9 +6,7 @@ g.reset().setColor(color).drawImage(require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA==")), this.x + 1, 0); } - WIDGETS["widhwt"] = { area: "tr", width: 26, draw: draw }; - - Bangle.on('swipe', function() { + function startTimer() { color = 0x41f; Bangle.buzz(); Bangle.drawWidgets(); @@ -17,6 +15,14 @@ Bangle.buzz(1E3, 1); Bangle.drawWidgets(); }, 35E3); + } - }); + if (process.env.HWVERSION == 1) { + WIDGETS["widhwt"] = { + area: "tr", + width: 26, + draw: draw, + }; + Bangle.on('swipe', startTimer); + } })(); diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog index 084ca6ed5..2ca405365 100644 --- a/apps/wohrm/ChangeLog +++ b/apps/wohrm/ChangeLog @@ -5,4 +5,7 @@ 0.05: Improved buzz timing and rendering 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.07: Home button fixed and README added -0.08: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) +0.08: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799) +0.09: Ported to Bangle.js2 + Home returns to clock, instead of menu + Add settings diff --git a/apps/wohrm/README.md b/apps/wohrm/README.md index ad9e82525..87b1a65da 100644 --- a/apps/wohrm/README.md +++ b/apps/wohrm/README.md @@ -8,6 +8,9 @@ and will notify you with a buzz whenever your heart rate falls below or jumps ab [Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). ## Setting the limits + +Use the settings menu to set the limits. On the Bangle.js1 these can in addition be set with the buttons: + For setting the lower limit press button 4 (left part of the watch's touch screen). Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. @@ -22,7 +25,7 @@ the received value: For 85% and above the bars are green, between 84% and 50% th and below 50% they turn red. ## Closing the app -Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. +Pressing middle button will switch off the HRM of the watch and return you to the launcher. # HRM usage The HRM is switched on when the app is started. It stays switch on while the app is running, even diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index c9c060e99..ab579463c 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -1,327 +1,400 @@ -/* eslint-disable no-undef */ -const Setter = { - NONE: "none", - UPPER: 'upper', - LOWER: 'lower' -}; - -const shortBuzzTimeInMs = 80; -const longBuzzTimeInMs = 400; - -let upperLimit = 130; -let upperLimitChanged = true; - -let lowerLimit = 100; -let lowerLimitChanged = true; - -let limitSetter = Setter.NONE; - -let currentHeartRate = 0; -let hrConfidence = -1; -let hrChanged = true; -let confidenceChanged = true; - -let setterHighlightTimeout; - -function renderUpperLimitBackground() { - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - g.fillRect(180,70, 210, 200); - - //Round top left corner - g.fillEllipse(115,40,135,70); - - //Round top right corner - g.setColor(0,0,0); - g.fillRect(205,40, 210, 45); - g.setColor(1,0,0); - g.fillEllipse(190,40,210,50); - - //Round inner corner - g.fillRect(174,71, 179, 76); - g.setColor(0,0,0); - g.fillEllipse(160,71,179,82); - - //Round bottom - g.setColor(1,0,0); - g.fillEllipse(180,190, 210, 210); -} - -function renderLowerLimitBackground() { - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - g.fillRect(10, 50, 40, 180); - - //Rounded top - g.setColor(0,0,1); - g.fillEllipse(10,40, 40, 60); - - //Round bottom right corner - g.setColor(0,0,1); - g.fillEllipse(90,180,110,210); - - //Round inner corner - g.setColor(0,0,1); - g.fillRect(40,175,45,180); - g.setColor(0,0,0); - g.fillEllipse(41,170,60,179); - - //Round bottom left corner - g.setColor(0,0,0); - g.fillRect(10,205, 15, 210); - g.setColor(0,0,1); - g.fillEllipse(10,200,30,210); -} - -function drawTrainingHeartRate() { - //Only redraw if the display is on - if (Bangle.isLCDOn()) { - renderUpperLimit(); - - renderCurrentHeartRate(); - - renderLowerLimit(); - - renderConfidenceBars(); - } - - buzz(); -} - -function renderUpperLimit() { - if(!upperLimitChanged) { return; } - - g.setColor(1,0,0); - g.fillRect(125,40, 210, 70); - - if(limitSetter === Setter.UPPER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Upper: " + upperLimit, 125, 50); - - upperLimitChanged = false; -} - -function renderCurrentHeartRate() { - if(!hrChanged) { return; } - - g.setColor(255,255,255); - g.fillRect(55, 110, 165, 150); - - g.setColor(0,0,0); - g.setFontVector(24); - g.setFontAlign(1, -1, 0); - g.drawString(currentHeartRate, 130, 117); - - //Reset alignment to defaults - g.setFontAlign(-1, -1, 0); - - hrChanged = false; -} - -function renderLowerLimit() { - if(!lowerLimitChanged) { return; } - - g.setColor(0,0,1); - g.fillRect(10, 180, 100, 210); - - if(limitSetter === Setter.LOWER){ - g.setColor(255,255, 0); - } else { - g.setColor(255,255,255); - } - g.setFontVector(13); - g.drawString("Lower: " + lowerLimit, 20,190); - - lowerLimitChanged = false; -} - -function renderConfidenceBars(){ - if(!confidenceChanged) { return; } - - if(hrConfidence >= 85){ - g.setColor(0, 255, 0); - } else if (hrConfidence >= 50) { - g.setColor(255, 255, 0); - } else if(hrConfidence >= 0){ - g.setColor(255, 0, 0); - } else { - g.setColor(255, 255, 255); - } - - g.fillRect(45, 110, 55, 150); - g.fillRect(165, 110, 175, 150); - - confidenceChanged = false; -} - -function renderPlusMinusIcons() { - if (limitSetter === Setter.NONE) { - g.setColor(0, 0, 0); - } else { - g.setColor(1, 1, 1); - } - - g.setFontVector(14); - - //+ for Btn1 - g.drawString("+", 222, 50); - - //- for Btn3 - g.drawString("-", 222,165); - - return; -} - -function renderHomeIcon() { - //Home for Btn2 - g.setColor(1, 1, 1); - g.drawLine(220, 118, 227, 110); - g.drawLine(227, 110, 234, 118); - - g.drawPoly([222,117,222,125,232,125,232,117], false); - g.drawRect(226,120,229,125); -} - -function buzz() { - // Do not buzz if not confident - if(hrConfidence < 85) { return; } - - if(currentHeartRate > upperLimit) - { - Bangle.buzz(shortBuzzTimeInMs); - setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); - } - - if(currentHeartRate < lowerLimit) - { - Bangle.buzz(longBuzzTimeInMs); - } -} - -function onHrm(hrm){ - if(currentHeartRate !== hrm.bpm){ - currentHeartRate = hrm.bpm; - hrChanged = true; - } - - if(hrConfidence !== hrm.confidence) { - hrConfidence = hrm.confidence; - confidenceChanged = true; - } -} - -function setLimitSetterToLower() { - resetHighlightTimeout(); - - limitSetter = Setter.LOWER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderUpperLimit(); - renderLowerLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToUpper() { - resetHighlightTimeout(); - - limitSetter = Setter.UPPER; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function setLimitSetterToNone() { - limitSetter = Setter.NONE; - - upperLimitChanged = true; - lowerLimitChanged = true; - - renderLowerLimit(); - renderUpperLimit(); - renderPlusMinusIcons(); -} - -function incrementLimit() { - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit++; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit++; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function decrementLimit(){ - resetHighlightTimeout(); - - if (limitSetter === Setter.UPPER) { - upperLimit--; - renderUpperLimit(); - upperLimitChanged = true; - } else if(limitSetter === Setter.LOWER) { - lowerLimit--; - renderLowerLimit(); - lowerLimitChanged = true; - } -} - -function resetHighlightTimeout() { - if (setterHighlightTimeout) { - clearTimeout(setterHighlightTimeout); - } - - setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); -} - -function switchOffApp(){ - Bangle.setHRMPower(0,"wohrm"); - Bangle.showLauncher(); -} - -Bangle.on('lcdPower', (on) => { - g.clear(); - if (on) { - Bangle.drawWidgets(); - - renderHomeIcon(); - renderLowerLimitBackground(); - renderUpperLimitBackground(); - lowerLimitChanged = true; - upperLimitChanged = true; - drawTrainingHeartRate(); - } -}); - -Bangle.setHRMPower(1,"wohrm"); -Bangle.on('HRM', onHrm); - -setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); -setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); -setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); - -setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); - -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -renderHomeIcon(); -renderLowerLimitBackground(); -renderUpperLimitBackground(); - -setInterval(drawTrainingHeartRate, 1000); +/* eslint-disable no-undef */ +const Setter = { + NONE: "none", + UPPER: 'upper', + LOWER: 'lower' +}; +const SETTINGS_FILE = "wohrm.setting.json"; +var settings = require('Storage').readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 +}; + +const shortBuzzTimeInMs = 80; +const longBuzzTimeInMs = 400; + +let upperLimitChanged = true; +let lowerLimitChanged = true; + +let limitSetter = Setter.NONE; + +let currentHeartRate = 0; +let hrConfidence = -1; +let hrChanged = true; +let confidenceChanged = true; + +let setterHighlightTimeout; + +const isB1 = process.env.HWVERSION==1; +const upperLshape = isB1 ? { + right: 125, + left: 210, + bottom: 40, + top: 210, + rectWidth: 30, + cornerRoundness: 5, + orientation: -1, + color: '#f00' +} : { + right: Bangle.appRect.x2-100, + left: Bangle.appRect.x2, + bottom: 24, + top: Bangle.appRect.y2, + rectWidth: 26, + cornerRoundness: 4, + orientation: -1, // rotated 180° + color: '#f00' +}; + +const lowerLshape = { + left: isB1 ? 10 : Bangle.appRect.x, + right: 100, + bottom: upperLshape.top, + top: upperLshape.bottom, + rectWidth: upperLshape.rectWidth, + cornerRoundness: upperLshape.cornerRoundness, + orientation: 1, + color: '#00f' +}; + +const centerBar = { + minY: (upperLshape.bottom + upperLshape.top - upperLshape.rectWidth)/2, + maxY: (upperLshape.bottom + upperLshape.top + upperLshape.rectWidth)/2, + confidenceWidth: isB1 ? 10 : 8, + minX: isB1 ? 55 : upperLshape.rectWidth + 14, + maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14 +}; + +const fontSizes = isB1 ? { + limits: 13, + heartRate: 24 +} : { + limits: 12, + heartRate: 20 +}; + +function fillEllipse(x, y, x2, y2) { + g.fillEllipse(Math.min(x, x2), + Math.min(y, y2), + Math.max(x, x2), + Math.max(y, y2)); +} + +/** + * @param p.left: the X coordinate of the left side of the L in its orientation + * @param p.right: the X coordinate of the right side of the L in its orientation + * @param p.top: the Y coordinate of the top side of the L in its orientation + * @param p.bottom: the Y coordinate of the bottom side of the L in its orientation + * @param p.strokeWidth: how thick we draw the letter. + * @param p.cornerRoundness: how much the corners should be rounded + * @param p.orientation: 1 == turned 0°; -1 == turned 180° + * @param p.color: the color to draw the shape + */ +function renderLshape(p) { + g.setColor(p.color); + + g.fillRect(p.right, p.bottom, p.left, p.bottom-p.orientation*p.rectWidth); + g.fillRect(p.left+p.orientation*p.rectWidth, + p.bottom-p.orientation*p.rectWidth, + p.left, + p.top+p.orientation*p.cornerRoundness*2); + + //Round end of small line + fillEllipse(p.right+p.orientation*p.cornerRoundness*2, + p.bottom, + p.right-p.orientation*p.cornerRoundness*2, + p.bottom-p.orientation*p.rectWidth); + + //Round outer corner + g.setColor(g.theme.bg); + g.fillRect(p.left+p.orientation*p.cornerRoundness, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness); + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.cornerRoundness*4, + p.bottom, + p.left, + p.bottom-p.orientation*p.cornerRoundness*2); + + //Round inner corner + g.fillRect(p.left+p.orientation*(p.rectWidth+p.cornerRoundness+1), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness-1)); + g.setColor(g.theme.bg); + fillEllipse(p.left+p.orientation*(p.rectWidth+p.cornerRoundness*4), + p.bottom-p.orientation*(p.rectWidth+1), + p.left+p.orientation*(p.rectWidth+1), + p.bottom-p.orientation*(p.rectWidth+p.cornerRoundness*3-1)); + + //Round end of long line + g.setColor(p.color); + fillEllipse(p.left+p.orientation*p.rectWidth, + p.top+p.orientation*p.cornerRoundness*4, + p.left, + p.top); +} + +function drawTrainingHeartRate() { + //Only redraw if the display is on + if (Bangle.isLCDOn()) { + renderUpperLimit(); + + renderCurrentHeartRate(); + + renderLowerLimit(); + + renderConfidenceBars(); + } + + buzz(); +} + +function renderUpperLimit() { + if(!upperLimitChanged) { return; } + + renderLshape(upperLshape); + + if(limitSetter === Setter.UPPER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Upper: " + settings.upperLimit, + upperLshape.right, + upperLshape.bottom+upperLshape.rectWidth/2); + + upperLimitChanged = false; +} + +function renderCurrentHeartRate() { + if(!hrChanged) { return; } + + g.setColor(g.theme.fg); + g.fillRect(centerBar.minX, centerBar.minY, + centerBar.maxX, centerBar.maxY); + + g.setColor(g.theme.bg); + g.setFontVector(fontSizes.heartRate); + g.setFontAlign(1, 0, 0); + g.drawString(currentHeartRate, + Math.max(upperLshape.right+upperLshape.cornerRoundness, + lowerLshape.right-lowerLshape.cornerRoundness), + (centerBar.minY+centerBar.maxY)/2); + + //Reset alignment to defaults + g.setFontAlign(-1, -1, 0); + + hrChanged = false; +} + +function renderLowerLimit() { + if(!lowerLimitChanged) { return; } + + renderLshape(lowerLshape); + + if(limitSetter === Setter.LOWER){ + g.setColor(1,1,0); + } else { + g.setColor(g.theme.fg); + } + g.setFontVector(fontSizes.limits).setFontAlign(-1, 0, 0); + g.drawString("Lower: " + settings.lowerLimit, + lowerLshape.left + lowerLshape.rectWidth/2, + lowerLshape.bottom - lowerLshape.rectWidth/2); + + lowerLimitChanged = false; +} + +function renderConfidenceBars(){ + if(!confidenceChanged) { return; } + + if(hrConfidence >= 85){ + g.setColor(0, 1, 0); + } else if (hrConfidence >= 50) { + g.setColor(1, 1, 0); + } else if(hrConfidence >= 0){ + g.setColor(1, 0, 0); + } else { + g.setColor(g.theme.fg); + } + + g.fillRect(centerBar.minX-centerBar.confidenceWidth, centerBar.minY, centerBar.minX, centerBar.maxY); + g.fillRect(centerBar.maxX, centerBar.minY, centerBar.maxX+centerBar.confidenceWidth, centerBar.maxY); + + confidenceChanged = false; +} + +function renderPlusMinusIcons() { + if (limitSetter === Setter.NONE) { + g.setColor(g.theme.bg); + } else { + g.setColor(g.theme.fg); + } + + g.setFontVector(14); + + //+ for Btn1 + g.drawString("+", 222, 50); + + //- for Btn3 + g.drawString("-", 222,165); + + return; +} + +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function buzz() { + // Do not buzz if not confident + if(hrConfidence < 85) { return; } + + if(currentHeartRate > settings.upperLimit) + { + Bangle.buzz(shortBuzzTimeInMs); + setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); + } + + if(currentHeartRate < settings.lowerLimit) + { + Bangle.buzz(longBuzzTimeInMs); + } +} + +function onHrm(hrm){ + if(currentHeartRate !== hrm.bpm){ + currentHeartRate = hrm.bpm; + hrChanged = true; + } + + if(hrConfidence !== hrm.confidence) { + hrConfidence = hrm.confidence; + confidenceChanged = true; + } +} + +function setLimitSetterToLower() { + resetHighlightTimeout(); + + limitSetter = Setter.LOWER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderUpperLimit(); + renderLowerLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToUpper() { + resetHighlightTimeout(); + + limitSetter = Setter.UPPER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToNone() { + limitSetter = Setter.NONE; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function incrementLimit() { + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit++; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit++; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function decrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + settings.upperLimit--; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + settings.lowerLimit--; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function resetHighlightTimeout() { + if (setterHighlightTimeout) { + clearTimeout(setterHighlightTimeout); + } + + setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); +} + +function switchOffApp(){ + Bangle.setHRMPower(0,"wohrm"); + load(); +} + +Bangle.on('lcdPower', (on) => { + if (on) { + Bangle.drawWidgets(); + + if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + } + renderLshape(lowerLshape); + renderLshape(upperLshape); + lowerLimitChanged = true; + upperLimitChanged = true; + drawTrainingHeartRate(); + } +}); + +Bangle.setHRMPower(1,"wohrm"); +Bangle.on('HRM', onHrm); + +g.setTheme({bg:"#000",fg:"#fff",dark:true}); +g.reset(); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +if (typeof(BTN5) !== typeof(undefined)) { + renderHomeIcon(); + setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); + setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); + setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); + + setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); +} else { + setWatch(switchOffApp, BTN1, {edge:"falling", debounce:50, repeat:true}); +} + +setInterval(drawTrainingHeartRate, 1000); diff --git a/apps/wohrm/settings.js b/apps/wohrm/settings.js new file mode 100644 index 000000000..6d31688f4 --- /dev/null +++ b/apps/wohrm/settings.js @@ -0,0 +1,35 @@ +(function menu(back) { + const SETTINGS_FILE = "wohrm.setting.json"; + + // initialize with default settings... + const storage = require('Storage'); + var settings = storage.readJSON(SETTINGS_FILE, 1) || { + upperLimit: 130, + lowerLimit: 100 + }; + + function save() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Workout HRM' }, + '< Back': back, + 'Upper limit': { + value: settings.upperLimit, + min: 100, max: 200, + onchange: v => { + settings.upperLimit = v; + save(); + } + }, + 'Lower limit': { + value: settings.lowerLimit, + min: 50, max: 150, + onchange: v => { + settings.lowerLimit = v; + save(); + } + } + }); +}) diff --git a/modules/Layout.js b/modules/Layout.js index 65e9a8dc8..cb64183ea 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -34,7 +34,7 @@ layoutObject has: optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal layout, `c` is an array of more `layoutObject` - * `"v"` - Veritical layout, `c` is an array of more `layoutObject` + * `"v"` - Vertical layout, `c` is an array of more `layoutObject` * A `id` field. If specified the object is added with this name to the returned `layout` object, so can be referenced as `layout.foo` * A `font` field, eg `6x8` or `30%` to use a percentage of screen height @@ -261,6 +261,7 @@ Layout.prototype.render = function (l) { x,y+4 ], bg = l.selected?g.theme.bgH:g.theme.bg2; g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); + if (l.col) g.setColor(l.col); if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); }, "img":function(l){