diff --git a/README.md b/README.md index 2d0b54a7d..877fe5e2c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) +**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By +submitting code to this repository you confirm that you are happy with it being MIT licensed, +and that it is not licensed in another way that would make this impossible. + ## How does it work? * A list of apps is in `apps.json` @@ -32,6 +36,7 @@ easily distinguish between file types, we use the following: * `stuff.img` is an image * `stuff.app.js` is JS code for applications * `stuff.wid.js` is JS code for widgets +* `stuff.settings.js` is JS code for the settings menu * `stuff.boot.js` is JS code that automatically gets run at boot time * `stuff.json` is used for JSON settings for an app @@ -314,6 +319,48 @@ the data you require from Bangle.js. See [apps/gpsrec/interface.html](the GPS Recorder) for a full example. +### Adding configuration to the "Settings" menu + +Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings". +To do so, the app needs to include a `settings.js` file, containing a single function +that handles configuring the app. +When the app settings are opened, this function is called with one +argument, `back`: a callback to return to the settings menu. + +Example `settings.js` +```js +// make sure to enclose the function in parentheses +(function(back) { + let settings = require('Storage').readJSON('app.settings.json',1)||{}; + function save(key, value) { + settings[key] = value; + require('Storage').write('app.settings.json',settings); + } + const appMenu = { + '': {'title': 'App Settings'}, + '< Back': back, + 'Monkeys': { + value: settings.monkeys||12, + onchange: (m) => {save('monkeys', m)} + } + }; + E.showMenu(appMenu) +}) +``` +In this example the app needs to add both `app.settings.js` and +`app.settings.json` to `apps.json`: +```json + { "id": "app", + ... + "storage": [ + ... + {"name":"app.settings.js","url":"settings.js"}, + {"name":"app.settings.json","content":"{}"} + ] + }, +``` +That way removing the app also cleans up `app.settings.json`. + ## Coding hints - use `g.setFont(.., size)` to multiply the font size, eg ("6x8",3) : "18x24" diff --git a/apps.json b/apps.json index c6a7cb7a7..2cadf5b1b 100644 --- a/apps.json +++ b/apps.json @@ -78,13 +78,14 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.04", + "version":"0.05", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, "storage": [ {"name":"welcome.js","url":"welcome.js"}, {"name":"welcome.app.js","url":"app.js"}, + {"name":"welcome.settings.js","url":"settings.js"}, {"name":"welcome.img","url":"app-icon.js","evaluate":true} ] }, @@ -117,7 +118,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.08", + "version":"0.10", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ @@ -337,13 +338,16 @@ }, { "id": "widbatpc", "name": "Battery Level Widget (with percentage)", + "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.06", + "version":"0.07", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", "storage": [ - {"name":"widbatpc.wid.js","url":"widget.js"} + {"name":"widbatpc.wid.js","url":"widget.js"}, + {"name":"widbatpc.settings.js","url":"settings.js"}, + {"name":"widbatpc.settings.json","content": "{}"} ] }, { "id": "widbt", @@ -663,7 +667,7 @@ { "id": "miclock", "name": "Mixed Clock", "icon": "clock-mixed.png", - "version":"0.03", + "version":"0.04", "description": "A mix of analog and digital Clock", "tags": "clock", "type":"clock", @@ -865,6 +869,19 @@ {"name":"torch.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "wohrm", + "name": "Workout HRM", + "icon": "app.png", + "version":"0.05", + "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", + "tags": "hrm,workout", + "type": "app", + "allow_emulator":true, + "storage": [ + {"name":"wohrm.app.js","url":"app.js"}, + {"name":"wohrm.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "widid", "name": "Bluetooth ID Widget", "icon": "widget.png", @@ -894,7 +911,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.05", + "version":"0.06", "description": "Animated Mario clock, jumps to change the time!", "tags": "clock,mario,retro", "type": "clock", @@ -942,6 +959,19 @@ {"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true} ] }, + { "id": "dotclock", + "name": "Dot Clock", + "icon": "clock-dot.png", + "version":"0.01", + "description": "A Minimal Dot Analog Clock", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"dotclock.app.js","url":"clock-dot.js"}, + {"name":"dotclock.img","url":"clock-dot-icon.js","evaluate":true} + ] + }, { "id": "widtbat", "name": "Tiny Battery Widget", "icon": "widget.png", @@ -1001,7 +1031,7 @@ "name": "Touch Launcher", "shortName":"Menu", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", "type":"launch", @@ -1047,5 +1077,18 @@ "storage": [ {"name":"widmp.wid.js","url":"widget.js"} ] + }, + { "id": "minionclk", + "name": "Minion clock", + "icon": "minionclk.png", + "version": "0.01", + "description": "Minion themed clock.", + "tags": "clock,minion", + "type": "clock", + "allow_emulator": true, + "storage": [ + {"name":"minionclk.app.js","url":"app.js"}, + {"name":"minionclk.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog new file mode 100644 index 000000000..26f95bbde --- /dev/null +++ b/apps/dotclock/ChangeLog @@ -0,0 +1 @@ +0.01: Based on the Analog Clock app, minimal dot interface \ No newline at end of file diff --git a/apps/dotclock/clock-dot-icon.js b/apps/dotclock/clock-dot-icon.js new file mode 100644 index 000000000..7098cb51f --- /dev/null +++ b/apps/dotclock/clock-dot-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A/AGUJyAXtACeZBCAOJh/wC6IADC4gA/XEINJC64A/AHcP+ACD/4CBTB0Ph8A+ACBAIKoKC65HKC4gA/AAfACysM5gvjTBgNKC64A/AEWZBCAXdADa4XaH4A/AAgA==")) diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js new file mode 100644 index 000000000..a4b3f260f --- /dev/null +++ b/apps/dotclock/clock-dot.js @@ -0,0 +1,162 @@ +let g; +let Bangle; + +const locale = require('locale'); +const p = Math.PI / 2; +const pRad = Math.PI / 180; +const faceWidth = 100; // watch face radius +let timer = null; +let currentDate = new Date(); +let hourRadius = 60; +let minRadius = 80; +const centerPx = g.getWidth() / 2; + +const seconds = (angle) => { + const a = angle * pRad; + const x = centerPx + Math.sin(a) * faceWidth; + const y = centerPx - Math.cos(a) * faceWidth; + + // if 15 degrees, make hour marker larger + const radius = (angle % 15) ? 2 : 4; + g.fillCircle(x, y, radius); +}; + +const hourDot = (angle,radius) => { + const a = angle * pRad; + const x = centerPx + Math.sin(a) * hourRadius; + const y = centerPx - Math.cos(a) * hourRadius; + g.fillCircle(x, y, radius); +}; + +const minDot = (angle,radius) => { + const a = angle * pRad; + const x = centerPx + Math.sin(a) * minRadius; + const y = centerPx - Math.cos(a) * minRadius; + g.fillCircle(x, y, radius); +}; + +const drawAll = () => { + g.clear(); + currentDate = new Date(); + // draw hands first + onMinute(); + // draw seconds + const currentSec = currentDate.getSeconds(); + // draw all secs + + for (let i = 0; i < 60; i++) { + if (i > currentSec) { + g.setColor(0, 0, 0.6); + } else { + g.setColor(0.3, 0.3, 1); + } + seconds((360 * i) / 60); + } + onSecond(); +}; + +const resetSeconds = () => { + g.setColor(0, 0, 0.6); + for (let i = 0; i < 60; i++) { + seconds((360 * i) / 60); + } +}; + +const drawMin = () => { + g.setColor(0.5, 0.5, 0.5); + for (let i = 0; i < 60; i++) { + minDot((360 * i) / 60,1); + } +}; + +const drawHour = () => { + g.setColor(0.5, 0.5, 0.5); + for (let i = 0; i < 12; i++) { + hourDot((360 * 5 * i) / 60,1); + } +}; + +const onSecond = () => { + g.setColor(0.3, 0.3, 1); + seconds((360 * currentDate.getSeconds()) / 60); + if (currentDate.getSeconds() === 59) { + resetSeconds(); + onMinute(); + } + g.setColor(1, 0.7, 0.2); + currentDate = new Date(); + seconds((360 * currentDate.getSeconds()) / 60); + g.setColor(1, 1, 1); +}; + +const drawDate = () => { + g.reset(); + g.setColor(1, 1, 1); + g.setFont('6x8', 2); + + const dayString = locale.dow(currentDate, true); + // pad left date + const dateString = ((currentDate.getDate() < 10) ? '0' : '') + currentDate.getDate().toString(); + const dateDisplay = `${dayString} ${dateString}`; + // console.log(`${dayString}|${dateString}`); + // center date + const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2; + const t = centerPx - 6 ; + g.drawString(dateDisplay, l, t); + // console.log(l, t); +}; +const onMinute = () => { + if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) { + g.clear(); + resetSeconds(); + } + // clear existing hands + g.setColor(0, 0, 0); + hourDot((360 * currentDate.getHours()) / 12,4); + minDot((360 * currentDate.getMinutes()) / 60,3); + + // Hour + drawHour(); + // Minute + drawMin(); + + // get new date, then draw new hands + currentDate = new Date(); + g.setColor(1, 0, 0); + // Hour + hourDot((360 * currentDate.getHours()) / 12,4); + g.setColor(1, 0.9, 0.9); + // Minute + minDot((360 * currentDate.getMinutes()) / 60,3); + if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) { + Bangle.buzz(); + } + drawDate(); +}; + +const startTimers = () => { + timer = setInterval(onSecond, 1000); +}; + +Bangle.on('lcdPower', (on) => { + if (on) { + // g.clear(); + drawAll(); + startTimers(); + Bangle.drawWidgets(); + } else { + if (timer) { + clearInterval(timer); + } + } +}); + +g.clear(); +resetSeconds(); +startTimers(); +drawAll(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/dotclock/clock-dot.png b/apps/dotclock/clock-dot.png new file mode 100644 index 000000000..702ac9065 Binary files /dev/null and b/apps/dotclock/clock-dot.png differ diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 7f6fbaef9..43e073cd1 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -1,4 +1,4 @@ - +/* jshint esversion: 6 */ const distanceUnits = { // how many meters per X? "m" : 1, "yd" : 0.9144, @@ -387,4 +387,21 @@ var locales = { abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab", day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado", trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "certo", on: "ligado", off: "desligado" }}, + "cs_CZ": { + lang: "cs_CZ", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "Kč", + int_curr_symbol: " CZK", + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: {0:"dop",1:"odp"}, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%d. %b %Y", 1: "%d.%m.%Y" }, // "30. led 2020" // "30.01.2020"(short) + abmonth: "led,úno,bře,dub,kvě,čvn,čvc,srp,zář,říj,lis,pro", + month: "leden,únor,březen,duben,květen,červen,červenec,srpen,září,říjen,listopad,prosinec", + abday: "ne,po,út,st,čt,pá,so", + day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota", + trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" }} }; diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index 74db9bc18..dfad2d26a 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -3,3 +3,4 @@ 0.03: use short date format from locale, take timeout from settings 0.04: modify date to display to be more at the original idea but still localized 0.05: use 12/24 hour clock from settings +0.06: Performance refactor, and enhanced graphics! diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 2eeb21c97..5622c10f2 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -3,6 +3,7 @@ + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock + Converting images to 1bit BMP: Image > Mode > Indexed and tick the "Use black and white (1-bit) palette", Then export as BMP. + Online Image convertor: https://www.espruino.com/Image+Converter + + Images must be converted 1Bit White/Black !!! Not Black/White **********************************/ const locale = require("locale"); @@ -16,110 +17,32 @@ let W, H; let intervalRef, displayTimeoutRef = null; -// Space to draw watch widgets (e.g battery, bluetooth status) -const WIDGETS_GUTTER = 10; - // Colours const LIGHTEST = "#effedd"; const LIGHT = "#add795"; const DARK = "#588d77"; const DARKEST = "#122d3e"; -// Mario Images -const marioRunningImage1 = { - width : 15, height : 20, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("B8AfwH+B/8f/z4M+KExcnAUSCw87w4L8CJQRNB/YH+AxgCEAPAA=")) -}; - -const marioRunningImage1Neg = { - width : 15, height : 20, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAHwB0DOgY/jt8PDAPAEAB2gOyAAgAAAOAB4AAAA=")) -}; - -const marioRunningImage2 = { - width : 15, height : 20, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("B8AfwH+B/8f/z4M+KExcnAUSCw87w4J6BEsPnSfyT+S+OMAAAAA=")) -}; - -const marioRunningImage2Neg = { - width : 15, height : 20, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAHwB0DOgY/jt8PDAPAGEA7QAYhgMMBhAAAAAAAA=")) -}; - -const pyramid = { - width : 20, height : 20, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAkAAQgAIEAEAgCAEBAAggAEQAAoAAE=")) -}; - -const pipe = { - width : 9, height : 6, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("/8BxaCRSCA==")) -}; - -const floor = { - width : 8, height : 3, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("/6pE")) -}; - -const sky = { - width : 128, height : 30, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("VVVVVVVVVVVVVVVVVVVVVQAAAAAAAAAAAAAAAAAAAABVVVVVVVVVVVVVVVVVVVVVIiIiIiIiIiIiIiIiIiIiIlVVVVVVVVVVVVVVVVVVVVWIiIiIiIiIiIiIiIiIiIiIVVVVVVVVVVVVVVVVVVVVVSIiIiIiIiICIiIiIiIiIiJVVVVVVVVVAVVVVVVVVVVViIiIiIiIiACIiIiIiIiIiFVVVVVVVVQAVVVVVVVVVVUiIiIiIiIgACIiIiIiIiIiVVVVVVVVUAAVVVUBVVVVUKqqqqqqqggAKCqqAKqqqoBVUBVVVVQAABAVVABVVVUAIiACIiIgAAAgAiAAIiIiAFVABVVVUAAAUAVUABRVVQCqgAKCqqAAACAAAAAgCqoAVUABAVVAAAAAAAAAAAVQAKqAAACqoAAAAAAAAAACgABAAAAAUEAAAAAAAAAABQAAgAAAACAAAAAAAAAAAAIAAAAAAABQAAAAAAAAAAAEAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) -}; - -const brick = { - width : 21, height : 15, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("f//0AABoAAsAABgAAMAABgAAMAABgAAMAABgAAMAABoAAsAABf//wA==")) -}; - -const flower = { - width : 7, height : 7, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("fY3wjW+OAA==")) -}; - -const pipePlant = { - width : 9, height : 15, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("FBsNhsHDWPn/gOLQSKQSKQQ=")) -}; - const marioSprite = { frameIdx: 0, - frames: [ - marioRunningImage1, - marioRunningImage2 - ], - negFrames: [ - marioRunningImage1Neg, - marioRunningImage2Neg - ], x: 35, y: 55, jumpCounter: 0, - jumpIncrement: Math.PI / 10, + jumpIncrement: Math.PI / 6, isJumping: false }; -const STATIC_TILES = { - "_": {img: floor, x: 16 * 8, y: 75}, - "X": {img: sky, x: 0, y: 10}, - "#": {img: brick, x: 0, y: 0}, +const coinSprite = { + frameIdx: 0, + x: 34, + y: 18, + isAnimating: false, + yDefault: 18, }; -const TILES = { - "T": {img: pipe, x: 16 * 8, y: 69}, - "^": {img: pyramid, x: 16 * 8, y: 55}, - "*": {img: flower, x: 16 * 8, y: 68}, - "V": {img: pipePlant, x: 16 * 8, y: 60} +const pyramidSprite = { + x: 90, + height: 34, }; const ONE_SECOND = 1000; @@ -136,95 +59,133 @@ function incrementTimer() { } } -function drawTile(sprite) { - g.drawImage(sprite.img, sprite.x, sprite.y); -} - function drawBackground() { + // Clear screen g.setColor(LIGHTEST); g.fillRect(0, 10, W, H); - // draw floor - g.setColor(DARK); - for (var x = 0; x < 16; x++) { - var floorSprite = Object.assign({}, STATIC_TILES._, {x: x * 8}); - drawTile(floorSprite); - } + // Date bar + g.setColor(DARKEST); + g.fillRect(0, 0, W, 9); // draw sky - var skySprite = STATIC_TILES.X; g.setColor(LIGHT); - drawTile(skySprite); + g.fillRect(0, 10, g.getWidth(), 15); + g.fillRect(0, 17, g.getWidth(), 17); + g.fillRect(0, 19, g.getWidth(), 19); + g.fillRect(0, 21, g.getWidth(), 21); } -function drawScenery() { - // new random sprite - const spriteKeys = Object.keys(TILES); - const key = spriteKeys[Math.floor(Math.random() * spriteKeys.length)]; - let newSprite = Object.assign({}, TILES[key]); +function drawFloor() { + const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image + for (let x = 0; x < 4; x++) { + g.drawImage(fImg, x * 20, g.getHeight() - 5); + } +} + +function drawPyramid() { + const pPol = [pyramidSprite.x + 10, H - 6, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 6]; // Pyramid poly + + g.setColor(LIGHT); + g.fillPoly(pPol); + + pyramidSprite.x -= 1; + // Reset and randomize pyramid if off-screen + if (pyramidSprite.x < - 100) { + pyramidSprite.x = 90; + pyramidSprite.height = Math.floor(Math.random() * (60 /* max */ - 25 /* min */ + 1) + 25 /* min */); + } +} + +function drawTreesFrame(x, y) { + const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image + + g.drawImage(tImg, x, y); + g.setColor(DARKEST); + g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6); +} + +function drawTrees() { + let newSprite = {x: 90, y: Math.floor(Math.random() * (40 /* max */ - 5 /* min */ + 1) + 15 /* min */)}; // remove first sprite if offscreen let firstBackgroundSprite = backgroundArr[0]; if (firstBackgroundSprite) { - if (firstBackgroundSprite.x < -20) backgroundArr.splice(0, 1); + if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1); } // set background sprite if array empty - var lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; + let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; if (!lastBackgroundSprite) { lastBackgroundSprite = newSprite; backgroundArr.push(lastBackgroundSprite); } // add random sprites - if (backgroundArr.length < 6 && lastBackgroundSprite.x < (16 * 7)) { - var randIdx = Math.floor(Math.random() * 25); - if (randIdx < spriteKeys.length - 1) { + if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) { + const randIdx = Math.floor(Math.random() * 25); + if (randIdx < 2) { backgroundArr.push(newSprite); } } for (x = 0; x < backgroundArr.length; x++) { let scenerySprite = backgroundArr[x]; - - // clear sprite at previous position - g.setColor(LIGHTEST); - drawTile(scenerySprite); - - // draw sprite in new position - g.setColor(LIGHT); scenerySprite.x -= 5; - drawTile(scenerySprite); + drawTreesFrame(scenerySprite.x, scenerySprite.y); } } -function drawMario() { - // clear old mario frame - g.setColor(LIGHTEST); - g.drawImage( - marioSprite.negFrames[marioSprite.frameIdx], - marioSprite.x, - marioSprite.y - ); - g.drawImage( - marioSprite.frames[marioSprite.frameIdx], - marioSprite.x, - marioSprite.y - ); +function drawCoinFrame(x, y) { + const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image + g.drawImage(cImg, x, y); +} +function drawCoin() { + if (!coinSprite.isAnimating) return; + + coinSprite.y -= 8; + if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) { + coinSprite.isAnimating = false; + coinSprite.y = coinSprite.yDefault; + return; + } + + drawCoinFrame(coinSprite.x, coinSprite.y); +} + +function drawMarioFrame(idx, x, y) { + const mFr1 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw52FSg2HAAIoDAgIOMB5AAFGQTtKeBLuNcQwOJFwgJFA=")); // Mario Frame 1 + const mFr2 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIdCCgTOIFgiVHFwYCBUhA9FBwz8HAo73GACQA=")); // Mario frame 2 + + switch(idx) { + case 0: + g.drawImage(mFr1, x, y); + break; + case 1: + g.drawImage(mFr2, x, y); + break; + default: + } +} + +function drawMario(date) { // calculate jumping - const t = new Date(), - seconds = t.getSeconds(), - milliseconds = t.getMilliseconds(); + const seconds = date.getSeconds(), + milliseconds = date.getMilliseconds(); if (seconds == 59 && milliseconds > 800 && !marioSprite.isJumping) { marioSprite.isJumping = true; } if (marioSprite.isJumping) { - marioSprite.y = (Math.sin(marioSprite.jumpCounter) * -10) + 50 /* Mario Y base value */; + marioSprite.y = (Math.sin(marioSprite.jumpCounter) * -12) + 50 /* Mario Y base value */; marioSprite.jumpCounter += marioSprite.jumpIncrement; + if (parseInt(marioSprite.jumpCounter) === 2 && !coinSprite.isAnimating) { + coinSprite.isAnimating = true; + } + if (marioSprite.jumpCounter.toFixed(1) >= 4) { marioSprite.jumpCounter = 0; marioSprite.isJumping = false; @@ -232,51 +193,28 @@ function drawMario() { } // calculate animation timing - if (timer % 100 === 0) { + if (timer % 50 === 0) { // shift to next frame marioSprite.frameIdx ^= 1; } - // colour in mario - g.setColor(LIGHT); - g.drawImage( - marioSprite.negFrames[marioSprite.frameIdx], - marioSprite.x, - marioSprite.y - ); - - // draw mario - g.setColor(DARKEST); - g.drawImage( - marioSprite.frames[marioSprite.frameIdx], - marioSprite.x, - marioSprite.y - ); + drawMarioFrame(marioSprite.frameIdx, marioSprite.x, marioSprite.y); } - -function drawBrick(x, y) { - const brickSprite = Object.assign({}, STATIC_TILES['#'], {x: x, y: y}); - - // draw brick background colour - g.setColor(LIGHT); - g.fillRect(x, y, x + 20, y+14); - - // draw brick sprite - g.setColor(DARK); - drawTile(brickSprite); +function drawBrickFrame(x, y) { + const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A")); + g.drawImage(brk, x, y); } -function drawTime() { +function drawTime(date) { // draw hour brick - drawBrick(20, 25); + drawBrickFrame(20, 25); // draw minute brick - drawBrick(42, 25); + drawBrickFrame(42, 25); - const t = new Date(); - const h = t.getHours(); + const h = date.getHours(); const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); - const mins = ("0" + t.getMinutes()).substr(-2); + const mins = ("0" + date.getMinutes()).substr(-2); g.setFont("6x8"); g.setColor(DARKEST); @@ -284,25 +222,30 @@ function drawTime() { g.drawString(mins, 47, 29); } -function drawDate() { +function drawDate(date) { g.setFont("6x8"); g.setColor(LIGHTEST); - let d = new Date(); - let dateStr = locale.date(d, true); - dateStr = dateStr.replace(d.getFullYear(), "").trim().replace(/\/$/i,""); - dateStr = locale.dow(d, true) + " " + dateStr; - g.drawString(dateStr, (W - g.stringWidth(dateStr))/2, 0, true); + let dateStr = locale.date(date, true); + dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); + dateStr = locale.dow(date, true) + " " + dateStr; + g.drawString(dateStr, (W - g.stringWidth(dateStr))/2, 1); } function redraw() { + const date = new Date(); + // Update timers incrementTimer(); // Draw frame - drawScenery(); - drawTime(); - drawDate(); - drawMario(); + drawBackground(); + drawFloor(); + drawPyramid(); + drawTrees(); + drawTime(date); + drawDate(date); + drawMario(date); + drawCoin(); // Render new frame g.flip(); @@ -335,7 +278,6 @@ function startTimers(){ resetDisplayTimeout(); - drawBackground(); redraw(); } @@ -345,7 +287,6 @@ function init() { // Initialise display Bangle.setLCDMode("80x80"); - g.clear(); // Store screen dimensions W = g.getWidth(); @@ -381,4 +322,4 @@ function init() { } // Initialise! -init(); +init(); \ No newline at end of file diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog index 9c2cccf09..f2e354bc1 100644 --- a/apps/miclock/ChangeLog +++ b/apps/miclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Localization +0.04: move jshint to the top diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js index cf6f4c196..bf6efb09e 100644 --- a/apps/miclock/clock-mixed.js +++ b/apps/miclock/clock-mixed.js @@ -1,5 +1,5 @@ -var locale = require("locale"); /* jshint esversion: 6 */ +var locale = require("locale"); const Radius = { "center": 8, "hour": 78, "min": 95, "dots": 102 }; const Center = { "x": 120, "y": 132 }; diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog new file mode 100755 index 000000000..7b83706bf --- /dev/null +++ b/apps/minionclk/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/minionclk/app-icon.js b/apps/minionclk/app-icon.js new file mode 100755 index 000000000..f78fb9e35 --- /dev/null +++ b/apps/minionclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhAaXlZEolVVvOj0mq1XOv9/qtWFb8rquj1XV5wBDAA2jvMqNLMqwAsCABBhBAIujqpbWvIsCLowBHMg0qFyVVFQJNDK4YoEAYxjFvIuQwHP6ur5+rDgfU5wBDMI2qCYOsC4XV0bFOwIWBAAeBAIOrMYYsF54sCCIWswQDB52AGBcrFIOtF4gAEMoJfDAAOrCYQuDAIowKFwQWIBIeA52pGQPWLYgAFA4YDBGA4uDEwQYFGQvPL4IuKC4wwHFglWAIQYIYoQuFCYwDGqrqFF4YYCYBKeHHwgRLlZhCLowMBKIIubWAtWF4PXBQRdEBAIBHGAoTRCIRfCDQQvBLofXB4NVBIQcDFweAOYdWp9WqwrDGA8ABoRJFAAOswFOqtUwBNFeQYVCwMqp4ABqwbDCowvCAgKOGf4N5aAIwKKIVPpwRBGQOBFw5fCIgZfGwFVlT/BqtVBQQwFUAVWFwMAlYvCL6mBqkqDgNUF4RLGAgVPFwMAklPwD0GX5gOCXoReBJgSvDIwWrAoN6py/Cp58DCYxQBVIYwHqyQBbgL6EX4qQDFwRdFLAifBaoQaEAJIuDCYWrEgoTIGAerWIJfHGRZdECZ5fD0bQBIwJgEIoynEGBJxIAAYPBwHNlUq6owBMIZ4OMKQPC0eiqsr53PX4guOwBhOComk0ejqpfB53OJZAeDU4lVvN5OQQXKBIeA0RfBvNV5wwBMI4uD1oLDFoN4AQJEMBYWl0fOL4NUL4QwCPw4BEwN4AAejvGACZaMBLoRfGAIWr1Z8HwGkFQRIBDgekwAVGFoOAB4QTDqgvBFwQDD1Wk1el1YsBv4oDAIoAB0d/GQOlAIV/CpF51ZfFAIgAEUoOiAJYmEAAoHDvOBFxWqAIWpFxwnDOJABBquJ1QwM5oDCMYYDDAIN/5+r6CABHRKOBmVewIwCYY4sC0bGB678B1ekZYIAB0ulwOlvWkIQLJCMY0yq2sr2sMJYABp96vQnB0tPz4FBBANzAAWj5pdI0dWq0zr2Ir2rMJKQCvNIp9PudIuYDBAIV0FwSMFL4d/LoMzL4WBwIwD6hhH5ujuZXBFAIDDAIOdFwPNL4hdCwBdCq15AgMrAQLDB52pRYYACBIWjvGdK4NJAQOdvIlB5oXB0QwBLoWjqsrAAaSCGANVGAJHBSQjBEAAINBewIDCLYIBCAAJfDv5dCLAIvBvIEDAAJoBwGqDQSUBY4htDBwIBBAoIwDL4WjvNdhAvCAAYFGhDGCY4IAB1QABFwQwDv4BB1V/0eA0mALINdP4IpGMYcAAIQABGAIxDAAIFBruCroAGq1eFALkDmdeD4IjDGQYCCBQYDBCgIqBAJAoBAQIDCmeBCoIDCGgIfBLooADL4YBCJAIiCAANdAIQoCAI4ABAYdWKQQDDMooEBlVPBwJfCGAwABFxABDSAZYGLo1OvFOIgQaBLYZhFAIsyFgYAEFAUqpxUBFYQ=")) \ No newline at end of file diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js new file mode 100755 index 000000000..88fe446ae --- /dev/null +++ b/apps/minionclk/app.js @@ -0,0 +1,68 @@ +const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA")); + +const locale = require("locale"); + +const black = 0x0000; +const white = 0xFFFF; + +let hour; +let minute; +let date; + +function draw() { + const d = new Date(); + + const newHour = ('0' + d.getHours()).substr(-2); + const newMinute = ('0' + d.getMinutes()).substr(-2); + const newDate = locale.date(d).trim(); + + g.setFontAlign(0, 0, 0); + + if (newHour !== hour) { + g.setFontVector(48); + g.setColor(black); + g.drawString(hour, 64, 92); + g.setColor(white); + g.drawString(newHour, 64, 92); + hour = newHour; + } + + if (newMinute !== minute) { + g.setFontVector(48); + g.setColor(black); + g.drawString(minute, 172, 92); + g.setColor(white); + g.drawString(newMinute, 172, 92); + minute = newMinute; + } + + if (newDate !== date) { + g.setFontVector(12); + g.setColor(black); + g.drawString(date, 120, 228); + g.setColor(0xFFFF); + g.drawString(newDate, 120, 228); + date = newDate; + } +} + +function drawAll() { + hour = ''; + minute = ''; + date = ''; + g.drawImage(bob, 0, 0, { scale: 4 }); + draw(); +} + +Bangle.on('lcdPower', function(on) { + if (on) { + drawAll(); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setInterval(draw, 1000); +drawAll(); + +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/minionclk/minionclk.png b/apps/minionclk/minionclk.png new file mode 100755 index 000000000..77cac31df Binary files /dev/null and b/apps/minionclk/minionclk.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 7b3511a89..71a8f8457 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -4,4 +4,9 @@ 0.05: Fix Settings json 0.06: Remove distance setting as there's a separate app for Locale now 0.07: Added vibrate as beep workaround -0.08: Added LCD wake-up settings \ No newline at end of file +<<<<<<< HEAD +0.08: Added LCD wake-up settings +======= +0.08: Add support for app/widget settings +0.09: Move Welcome into App/widget settings +>>>>>>> master diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 8746d121a..c35f48cc3 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -121,14 +121,6 @@ function showMainMenu() { } } }, - 'Welcome App': { - value: !settings.welcomed, - format: boolFormat, - onchange: v => { - settings.welcomed = v ? undefined : true; - updateSettings(); - } - }, 'Locale': showLocaleMenu, 'Select Clock': showClockMenu, 'HID': { @@ -141,6 +133,7 @@ function showMainMenu() { }, 'Set Time': showSetTimeMenu, 'LCD Wake-Up': showWakeUpMenu, + 'App/widget settings': showAppSettingsMenu, 'Reset Settings': showResetMenu, 'Turn Off': Bangle.off, '< Back': () => { load(); } @@ -408,4 +401,48 @@ function showSetTimeMenu() { return E.showMenu(timemenu); } +function showAppSettingsMenu() { + let appmenu = { + '': { 'title': 'App Settings' }, + '< Back': showMainMenu, + } + const apps = storage.list(/\.info$/) + .map(app => storage.readJSON(app, 1)) + .filter(app => app && app.settings) + .sort((a, b) => a.sortorder - b.sortorder) + if (apps.length === 0) { + appmenu['No app has settings'] = () => { }; + } + apps.forEach(function (app) { + appmenu[app.name] = () => { showAppSettings(app) }; + }) + E.showMenu(appmenu) +} +function showAppSettings(app) { + const showError = msg => { + E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); + setWatch(showAppSettingsMenu, BTN1, { repeat: false }); + } + let appSettings = storage.read(app.settings); + if (!appSettings) { + return showError('Missing settings'); + } + try { + appSettings = eval(appSettings); + } catch (e) { + console.log(`${app.name} settings error:`, e) + return showError('Error in settings'); + } + if (typeof appSettings !== "function") { + return showError('Invalid settings'); + } + try { + // pass showAppSettingsMenu as "back" argument + appSettings(showAppSettingsMenu); + } catch (e) { + console.log(`${app.name} settings error:`, e) + return showError('Error in settings'); + } +} + showMainMenu(); diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index bd3d5d225..f21553c54 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Add swipe support and doucle tap to run application \ No newline at end of file +0.02: Add swipe support and doucle tap to run application +0.03: Close launcher when lcd turn off \ No newline at end of file diff --git a/apps/toucher/app.js b/apps/toucher/app.js index 2b80198c9..7a3f6ff97 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -127,4 +127,8 @@ Bangle.on('touch', function(button){ Bangle.on('swipe', dir => { if(dir == 1) prev(); else next(); +}); + +Bangle.on('lcdPower', function(on) { + if(!on) return load(); }); \ No newline at end of file diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index d8d647138..34f6e3a82 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -2,3 +2,4 @@ 0.02: Animate balloon intro 0.03: BTN3 now won't restart when at the end 0.04: Fix regression after tweaks to Storage.readJSON +0.05: Move configuration into App/widget settings diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js new file mode 100644 index 000000000..2fbd585c6 --- /dev/null +++ b/apps/welcome/settings.js @@ -0,0 +1,16 @@ +// The welcome app is special, and gets to use global settings +(function(back) { + let settings = require('Storage').readJSON('setting.json', 1) || {} + E.showMenu({ + '': { 'title': 'Welcome App' }, + 'Run again': { + value: !settings.welcomed, + format: v => v ? 'Yes' : 'No', + onchange: v => { + settings.welcomed = v ? undefined : true + require('Storage').write('setting.json', settings) + }, + }, + '< Back': back, + }) +}) diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 3988729c3..245ec3af3 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -3,3 +3,4 @@ 0.04: Ensure redrawing works with variable size widget system 0.05: Change color depending on battery level, cloned from widbat 0.06: Show battery percentage as text +0.07: Add settings: percentage/color/charger icon diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js new file mode 100644 index 000000000..5c0bdbcae --- /dev/null +++ b/apps/widbatpc/settings.js @@ -0,0 +1,58 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'widbatpc.settings.json' + const COLORS = ['By Level', 'Green', 'Monochrome'] + + // initialize with default settings... + let s = { + 'color': COLORS[0], + 'percentage': true, + 'charger': true, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key] + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value + storage.write(SETTINGS_FILE, s) + WIDGETS["batpc"].reload() + } + } + + const onOffFormat = b => (b ? 'on' : 'off') + const menu = { + '': { 'title': 'Battery Widget' }, + '< Back': back, + 'Percentage': { + value: s.percentage, + format: onOffFormat, + onchange: save('percentage'), + }, + 'Charging Icon': { + value: s.charger, + format: onOffFormat, + onchange: save('charger'), + }, + 'Color': { + format: () => s.color, + onchange: function () { + // cycles through options + const oldIndex = COLORS.indexOf(s.color) + const newIndex = (oldIndex + 1) % COLORS.length + s.color = COLORS[newIndex] + save('color')(s.color) + }, + }, + } + E.showMenu(menu) +}) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 7100dc111..a679f6721 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,20 +1,62 @@ (function(){ +const DEFAULTS = { + 'color': 'By Level', + 'percentage': true, + 'charger': true, +} +const COLORS = { + 'white': -1, + 'charging': 0x07E0, // "Green" + 'high': 0x05E0, // slightly darker green + 'ok': 0xFD20, // "Orange" + 'low':0xF800, // "Red" +} +const SETTINGS_FILE = 'widbatpc.settings.json' + +let settings +function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} +} +function setting(key) { + if (!settings) { loadSettings() } + return (key in settings) ? settings[key] : DEFAULTS[key] +} + const levelColor = (l) => { - if (Bangle.isCharging()) return 0x07E0; // "Green" - if (l >= 50) return 0x05E0; // slightly darker green - if (l >= 15) return 0xFD20; // "Orange" - return 0xF800; // "Red" + // "charging" is very bright -> percentage is hard to read, "high" is ok(ish) + const green = setting('percentage') ? COLORS.high : COLORS.charging + switch (setting('color')) { + case 'Monochrome': return COLORS.white; // no chance of reading the percentage here :-( + case 'Green': return green; + case 'By Level': // fall through + default: + if (setting('charger')) { + // charger icon -> always make percentage readable + if (Bangle.isCharging() || l >= 50) return green; + } else { + // no icon -> brightest green to indicate charging, even when showing percentage + if (Bangle.isCharging()) return COLORS.charging; + if (l >= 50) return COLORS.high; + } + if (l >= 15) return COLORS.ok; + return COLORS.low; + } +} +const chargerColor = () => { + return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging } function setWidth() { - WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0); + WIDGETS["batpc"].width = 40; + if (Bangle.isCharging() && setting('charger')) { + WIDGETS["batpc"].width += 16; + } } function draw() { var s = 39; var x = this.x, y = this.y; - const l = E.getBattery(), c = levelColor(l); - if (Bangle.isCharging()) { - g.setColor(c).drawImage(atob( + if (Bangle.isCharging() && setting('charger')) { + g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); x+=16; } @@ -22,8 +64,13 @@ function draw() { g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); g.fillRect(x+s-3,y+10,x+s,y+14); + const l = E.getBattery(), + c = levelColor(l); g.setColor(c).fillRect(x+4,y+6,x+4+l*(s-12)/100,y+17); g.setColor(-1); + if (!setting('percentage')) { + return; + } g.setFontAlign(-1,-1); if (l >= 100) { g.setFont('4x6', 2); @@ -34,6 +81,16 @@ function draw() { g.drawString(l, x + 6, y + 4); } } +// reload widget, e.g. when settings have changed +function reload() { + loadSettings() + // need to redraw all widgets, because changing the "charger" setting + // can affect the width and mess with the whole widget layout + setWidth() + g.clear(); + Bangle.drawWidgets(); +} + Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); setWidth(); @@ -43,7 +100,7 @@ Bangle.on('charging',function(charging) { var batteryInterval; Bangle.on('lcdPower', function(on) { if (on) { - WIDGETS["bat"].draw(); + WIDGETS["batpc"].draw(); // refresh once a minute if LCD on if (!batteryInterval) batteryInterval = setInterval(draw, 60000); @@ -54,6 +111,6 @@ Bangle.on('lcdPower', function(on) { } } }); -WIDGETS["bat"]={area:"tr",width:40,draw:draw}; +WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); })() diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog new file mode 100644 index 000000000..e16b50c5b --- /dev/null +++ b/apps/wohrm/ChangeLog @@ -0,0 +1,5 @@ +0.01: Only tested on the emulator. +0.02: Adapted to new App code layout +0.03: Optimized rendering for the background +0.04: Only buzz on high confidence (>85%) +0.05: Improved buzz timing and rendering diff --git a/apps/wohrm/app-icon.js b/apps/wohrm/app-icon.js new file mode 100644 index 000000000..4a69b16bd --- /dev/null +++ b/apps/wohrm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AVgnd5tABI3c7oJGAAUs5gAC4gJDpgJD4QWGhoMDAAPQBJYADBgoABBJYAChgJD5oDC4AJEAAfAC4fcBIfUDYYJEEogWCgQJEoYSHAAsgIw3MmYqIn89JAoXFn5DH4f/+YXFWQnE/4GEAAXP///ZgooE4X/ngvMPAQXEBoIXHHIJfDC4ss5nf+f9OosjFwgXF5oTBp8z+gMBMQPTn5dBNIgXCAwPDEQM/mQmCJQNP/8zDIJRDO4SnB6fz7k/poXEJwIJBmanGhvMl//loxC7nE/jUCon/6gzBC4PQC4MDKIJFDn9M4YXB5nUKYbACmAXBgE/+YMBOoMvngXDJIKDB6YvBOwRgDaoINB788p5wDn7HELwQABghWCBoPD/s/YwNN5i+Bc4dAC4bBCC4fyPIPU+Z0BDAZGEJAffYgPC+ZxBG4KkB6f/C4JGEAAQsBcIX/+QEBCgP9A4IXBCwwwB5pxDPYJoDcgIuIGASJH5rvBAwIWIeYQABl5jBAAXDIwLrCABCcC76gDAoP0RgwAFYYJ7DJAcsFxYABaYJ7DAAXECxhJEAAgWOPQgACIpoADUwb1BCyBJERZgYKkAXUglACygA/AH4AFA==")) \ No newline at end of file diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js new file mode 100644 index 000000000..ff57c0b62 --- /dev/null +++ b/apps/wohrm/app.js @@ -0,0 +1,325 @@ +/* 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, 130,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 renderButtonIcons() { + g.setColor(255,255,255); + g.setFontVector(14); + + //+ for Btn1 + g.drawString("+", 222,50); + + //Home for Btn2 + 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); + + //- for Btn3 + g.drawString("-", 222,165); +} + +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; + console.log("Limit setter is lower"); + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderUpperLimit(); + renderLowerLimit(); +} + +function setLimitSetterToUpper() { + resetHighlightTimeout(); + + limitSetter = Setter.UPPER; + console.log("Limit setter is upper"); + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); +} + +function setLimitSetterToNone() { + limitSetter = Setter.NONE; + console.log("Limit setter is none"); + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); +} + +function incrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + upperLimit++; + renderUpperLimit(); + console.log("Upper limit: " + upperLimit); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + lowerLimit++; + renderLowerLimit(); + console.log("Lower limit: " + lowerLimit); + lowerLimitChanged = true; + } +} + +function decrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + upperLimit--; + renderUpperLimit(); + console.log("Upper limit: " + upperLimit); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + lowerLimit--; + renderLowerLimit(); + console.log("Lower limit: " + lowerLimit); + lowerLimitChanged = true; + } +} + +function resetHighlightTimeout() { + if (setterHighlightTimeout) { + clearTimeout(setterHighlightTimeout); + } + + setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); +} + +// Show launcher when middle button pressed +function switchOffApp(){ + Bangle.setHRMPower(0); + Bangle.showLauncher(); +} + +// special function to handle display switch on +Bangle.on('lcdPower', (on) => { + g.clear(); + if (on) { + Bangle.drawWidgets(); + renderButtonIcons(); + // call your app function here + renderLowerLimitBackground(); + renderUpperLimitBackground(); + lowerLimitChanged = true; + upperLimitChanged = true; + drawTrainingHeartRate(); + } +}); + +Bangle.setHRMPower(1); +Bangle.on('HRM', onHrm); + +setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); +setWatch(switchOffApp, BTN2, {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 }); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +//drawTrainingHeartRate(); + +renderButtonIcons(); +renderLowerLimitBackground(); +renderUpperLimitBackground(); + +// refesh every sec +setInterval(drawTrainingHeartRate, 1000); diff --git a/apps/wohrm/app.png b/apps/wohrm/app.png new file mode 100644 index 000000000..8f9c0ea5d Binary files /dev/null and b/apps/wohrm/app.png differ diff --git a/js/appinfo.js b/js/appinfo.js index 613d15379..f4ab498b1 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -60,6 +60,8 @@ var AppInfo = { if (app.type && app.type!="app") json.type = app.type; if (fileContents.find(f=>f.name==app.id+".app.js")) json.src = app.id+".app.js"; + if (fileContents.find(f=>f.name==app.id+".settings.js")) + json.settings = app.id+".settings.js"; if (fileContents.find(f=>f.name==app.id+".img")) json.icon = app.id+".img"; if (app.sortorder) json.sortorder = app.sortorder;