diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..e79f87a5d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,249 @@ +const lintExemptions = require("./apps/lint_exemptions.js"); +const fs = require("fs"); +const path = require("path"); + +function findGeneratedJS(roots) { + function* listFiles(dir, allow) { + for (const f of fs.readdirSync(dir)) { + const filepath = path.join(dir, f); + const stat = fs.statSync(filepath); + + if (stat.isDirectory()) { + yield* listFiles(filepath, allow); + } else if(allow(filepath)) { + yield filepath; + } + } + } + + return roots.flatMap(root => + [...listFiles(root, f => f.endsWith(".ts"))] + .map(f => f.replace(/\.ts$/, ".js")) + ); +} + +module.exports = { + "env": { + // TODO: "espruino": false + // TODO: "banglejs": false + // For a prototype of the above, see https://github.com/espruino/BangleApps/pull/3237 + }, + "extends": "eslint:recommended", + "globals": { + // Methods and Fields at https://banglejs.com/reference + "Array": "readonly", + "ArrayBuffer": "readonly", + "ArrayBufferView": "readonly", + "Bangle": "readonly", + "BluetoothDevice": "readonly", + "BluetoothRemoteGATTCharacteristic": "readonly", + "BluetoothRemoteGATTServer": "readonly", + "BluetoothRemoteGATTService": "readonly", + "Boolean": "readonly", + "console": "readonly", + "DataView": "readonly", + "Date": "readonly", + "E": "readonly", + "Error": "readonly", + "Flash": "readonly", + "Float32Array": "readonly", + "Float64Array": "readonly", + "Function": "readonly", + "Graphics": "readonly", + "I2C": "readonly", + "Int16Array": "readonly", + "Int32Array": "readonly", + "Int8Array": "readonly", + "InternalError": "readonly", + "JSON": "readonly", + "Math": "readonly", + "Modules": "readonly", + "NRF": "readonly", + "Number": "readonly", + "Object": "readonly", + "OneWire": "readonly", + "Pin": "readonly", + "process": "readonly", + "Promise": "readonly", + "ReferenceError": "readonly", + "RegExp": "readonly", + "Serial": "readonly", + "SPI": "readonly", + "StorageFile": "readonly", + "String": "readonly", + "SyntaxError": "readonly", + "TFMicroInterpreter": "readonly", + "TypeError": "readonly", + "Uint16Array": "readonly", + "Uint24Array": "readonly", + "Uint32Array": "readonly", + "Uint8Array": "readonly", + "Uint8ClampedArray": "readonly", + "Unistroke": "readonly", + "Waveform": "readonly", + // Methods and Fields at https://banglejs.com/reference + "__FILE__": "readonly", + "analogRead": "readonly", + "analogWrite": "readonly", + "arguments": "readonly", + "atob": "readonly", + "Bluetooth": "readonly", + "BTN": "readonly", + "BTN1": "readonly", + "BTN2": "readonly", + "BTN3": "readonly", + "BTN4": "readonly", + "BTN5": "readonly", + "btoa": "readonly", + "changeInterval": "readonly", + "clearInterval": "readonly", + "clearTimeout": "readonly", + "clearWatch": "readonly", + "decodeURIComponent": "readonly", + "digitalPulse": "readonly", + "digitalRead": "readonly", + "digitalWrite": "readonly", + "dump": "readonly", + "echo": "readonly", + "edit": "readonly", + "encodeURIComponent": "readonly", + "eval": "readonly", + "getPinMode": "readonly", + "getSerial": "readonly", + "getTime": "readonly", + "global": "readonly", + "HIGH": "readonly", + "I2C1": "readonly", + "Infinity": "readonly", + "isFinite": "readonly", + "isNaN": "readonly", + "LED": "readonly", + "LED1": "readonly", + "LED2": "readonly", + "load": "readonly", + "LoopbackA": "readonly", + "LoopbackB": "readonly", + "LOW": "readonly", + "NaN": "readonly", + "parseFloat": "readonly", + "parseInt": "readonly", + "peek16": "readonly", + "peek32": "readonly", + "peek8": "readonly", + "pinMode": "readonly", + "poke16": "readonly", + "poke32": "readonly", + "poke8": "readonly", + "print": "readonly", + "require": "readonly", + "reset": "readonly", + "save": "readonly", + "Serial1": "readonly", + "setBusyIndicator": "readonly", + "setInterval": "readonly", + "setSleepIndicator": "readonly", + "setTime": "readonly", + "setTimeout": "readonly", + "setWatch": "readonly", + "shiftOut": "readonly", + "SPI1": "readonly", + "Terminal": "readonly", + "trace": "readonly", + "VIBRATE": "readonly", + // Aliases and not defined at https://banglejs.com/reference + "g": "readonly", + "WIDGETS": "readonly", + "module": "readonly", + "exports": "writable", + "D0": "readonly", + "D1": "readonly", + "D2": "readonly", + "D3": "readonly", + "D4": "readonly", + "D5": "readonly", + "D6": "readonly", + "D7": "readonly", + "D8": "readonly", + "D9": "readonly", + "D10": "readonly", + "D11": "readonly", + "D12": "readonly", + "D13": "readonly", + "D14": "readonly", + "D15": "readonly", + "D16": "readonly", + "D17": "readonly", + "D18": "readonly", + "D19": "readonly", + "D20": "readonly", + "D21": "readonly", + "D22": "readonly", + "D23": "readonly", + "D24": "readonly", + "D25": "readonly", + "D26": "readonly", + "D27": "readonly", + "D28": "readonly", + "D29": "readonly", + "D30": "readonly", + "D31": "readonly", + + "bleServiceOptions": "writable", // available in boot.js code that's called ad part of bootupdate + }, + "parserOptions": { + "ecmaVersion": 11 + }, + "rules": { + "indent": [ + "off", + 2, + { + "SwitchCase": 1 + } + ], + "no-constant-condition": "off", + "no-delete-var": "off", + "no-empty": ["warn", { "allowEmptyCatch": true }], + "no-global-assign": "off", + "no-inner-declarations": "off", + "no-prototype-builtins": "off", + "no-redeclare": "off", + "no-unreachable": "warn", + "no-cond-assign": "warn", + "no-useless-catch": "warn", + "no-undef": "warn", + "no-unused-vars": ["warn", { "args": "none" } ], + "no-useless-escape": "off", + "no-control-regex" : "off" + }, + overrides: [ + { + files: ["*.ts"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + rules: { + "no-delete-var": "off", + "no-empty": ["error", { "allowEmptyCatch": true }], + "no-prototype-builtins": "off", + "prefer-const": "off", + "prefer-rest-params": "off", + "no-control-regex" : "off", + "@typescript-eslint/no-delete-var": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-var-requires": "off", + } + }, + ...Object.entries(lintExemptions).map(([filePath, {rules}]) => ({ + files: [filePath], + rules: Object.fromEntries(rules.map(rule => [rule, "off"])), + })), + ], + ignorePatterns: findGeneratedJS(["apps/", "modules/"]), + reportUnusedDisableDirectives: true, +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..69aa0ab3d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +Contributing to BangleApps +========================== + +https://github.com/espruino/BangleApps?tab=readme-ov-file#getting-started +has some links to tutorials on developing for Bangle.js. + +Please check out the Wiki to get an idea what sort of things +we'd like to see for contributed apps: https://github.com/espruino/BangleApps/wiki/App-Contribution + diff --git a/apps/_example_clock/app.js b/apps/_example_clock/app.js new file mode 100644 index 000000000..d1f997136 --- /dev/null +++ b/apps/_example_clock/app.js @@ -0,0 +1,48 @@ +{ + // timeout used to update every minute + let drawTimeout; + + // schedule a draw for the next minute + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function() { + // queue next draw in one minute + queueDraw(); + // Work out where to draw... + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date); + // draw time + g.setFontAlign(0,0).setFont("Vector",48); + g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 35; + g.setFontAlign(0,0).setFont("6x8"); + g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background + g.drawString(dateStr,x,y); + }; + + // Clear the screen once, at startup + g.clear(); + // draw immediately at first, queue update + draw(); + + // Show launcher when middle button pressed + Bangle.setUI({mode:"clock", remove:function() { + // free any memory we allocated to allow fast loading + }}); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} \ No newline at end of file diff --git a/apps/_example_clock/screenshot.png b/apps/_example_clock/screenshot.png new file mode 100644 index 000000000..32d910ef5 Binary files /dev/null and b/apps/_example_clock/screenshot.png differ diff --git a/apps/accelsender/ChangeLog b/apps/accelsender/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/accelsender/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/accelsender/README.md b/apps/accelsender/README.md new file mode 100644 index 000000000..eb18bb88a --- /dev/null +++ b/apps/accelsender/README.md @@ -0,0 +1,19 @@ +# Accerleration Data Provider + +This app provides acceleration data via Bluetooth, which can be used in Gadgetbridge. + +## Usage + +This boot code runs in the background and has no user interface. +Currently this app is used to enable Sleep as Android tracking for your Banglejs using Gadgetbridge. + +**Please Note**: This app only listens to "accel" events and sends them to your phone using Bluetooth. + +## Creator + +[Another Stranger](https://github.com/anotherstranger) + +## Aknowledgements + +Special thanks to [José Rebelo](https://github.com/joserebelo) and [Rob Pilling](https://github.com/bobrippling) +for their Code Reviews and guidance. diff --git a/apps/accelsender/bluetooth.png b/apps/accelsender/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/accelsender/bluetooth.png differ diff --git a/apps/accelsender/boot.js b/apps/accelsender/boot.js new file mode 100644 index 000000000..b1a076e2b --- /dev/null +++ b/apps/accelsender/boot.js @@ -0,0 +1,55 @@ +(() => { + /** + * Sends a message to the gadgetbridge via Bluetooth. + * @param {Object} message - The message to be sent. + */ + function gbSend(message) { + try { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } catch (error) { + console.error("Failed to send message via Bluetooth:", error); + } + } + + var max_acceleration = { x: 0, y: 0, z: 0, diff: 0, td: 0, mag: 0 }; + var hasData = false; + + /** + * Updates the maximum acceleration if the current acceleration is greater. + * @param {Object} accel - The current acceleration object with x, y, z, and mag properties. + */ + function updateAcceleration(accel) { + hasData = true; + var current_max_raw = accel.mag; + var max_raw = max_acceleration.mag; + + if (current_max_raw > max_raw) { + max_acceleration = accel; + } + } + + /** + * Updates the acceleration data and sends it to gadgetbridge. + * Resets the maximum acceleration. + * Note: If your interval setting is too short, the last value gets sent again. + */ + function sendAccelerationData() { + var accel = hasData ? max_acceleration : Bangle.getAccel(); + + var update_data = { + t: "accel", accel: accel + }; + gbSend(update_data); + + max_acceleration = { x: 0, y: 0, z: 0, mag: 0, diff: 0, td: 0 }; + hasData = false; + } + + var config = require("Storage").readJSON("accelsender.json") || {}; + if (config.enabled) { // Gadgetbridge needs to enable and disable tracking by writing {enabled: true} to "accelsender.json" and reloading + setInterval(sendAccelerationData, config.interval); + Bangle.on("accel", updateAcceleration); // Log all acceleration events + } + +})(); diff --git a/apps/accelsender/boot.min.js b/apps/accelsender/boot.min.js new file mode 100644 index 000000000..72a336083 --- /dev/null +++ b/apps/accelsender/boot.min.js @@ -0,0 +1 @@ +(()=>{function e(a){c=!0;a.mag>b.mag&&(b=a)}function f(){var a={t:"accel",accel:c?b:Bangle.getAccel()};try{Bluetooth.println(""),Bluetooth.println(JSON.stringify(a))}catch(g){console.error("Failed to send message via Bluetooth:",g)}b={x:0,y:0,z:0,mag:0,diff:0,td:0};c=!1}var b={x:0,y:0,z:0,diff:0,td:0,mag:0},c=!1,d=require("Storage").readJSON("accelsender.json")||{};d.enabled&&(setInterval(f,d.interval),Bangle.on("accel",e))})(); diff --git a/apps/accelsender/config.json b/apps/accelsender/config.json new file mode 100644 index 000000000..70590f2f2 --- /dev/null +++ b/apps/accelsender/config.json @@ -0,0 +1,4 @@ +{ + "enabled": false, + "interval": 10000 +} \ No newline at end of file diff --git a/apps/accelsender/metadata.json b/apps/accelsender/metadata.json new file mode 100644 index 000000000..b63f7485e --- /dev/null +++ b/apps/accelsender/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "accelsender", + "name": "Acceleration Data Provider", + "shortName": "Accel Data Provider", + "version": "0.01", + "description": "This app sends accelerometer and heart rate data from your Bangle.js via Bluetooth.", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "accel", + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "accelsender.boot.js", + "url": "boot.min.js" + } + ], + "data": [ + { + "name": "accelsender.json", + "url": "config.json" + } + ] +} diff --git a/apps/analogquadclk/ChangeLog b/apps/analogquadclk/ChangeLog new file mode 100644 index 000000000..d1bd8762f --- /dev/null +++ b/apps/analogquadclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New Clock! +0.02: Fix fastloading memory leak and clockinfo overwritten by hands +0.03: Use new clockinfo lib with function to render images wirh borders \ No newline at end of file diff --git a/apps/analogquadclk/app-icon.js b/apps/analogquadclk/app-icon.js new file mode 100644 index 000000000..8eedcb6cb --- /dev/null +++ b/apps/analogquadclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X//8HA4IEBgH4C5cFqgJHitQjWpBY9q0gLvI5ar/AAkgBRMC1ALJlX6CxOrBZMq34LJ1f/9QKHhW//2gCxP6wAWHy/+KREqq4WIgGtr+qLhG1vw5IgX1KBALBywWIIwNaHJEAlNqUZOltAuJyouKqwuKrQuhywuJNIIuJlIuJHQLGIBYQ6IgtU1Q6GitQjWplQVGtWkBYIhHBcpHBBY5HBM5IABA")) \ No newline at end of file diff --git a/apps/analogquadclk/app.js b/apps/analogquadclk/app.js new file mode 100644 index 000000000..dbaa49825 --- /dev/null +++ b/apps/analogquadclk/app.js @@ -0,0 +1,247 @@ +{ + const W = g.getWidth(); + const H = g.getHeight(); + const background = require("clockbg"); // image backgrounds + let drawTimeout; // timeout used to update every minute + let date = new Date(); // date at last draw + let lastModified = {x1:0,y1:0,x2:W-1,y2:H-1,first:true}; // rect that was covered by hands + + const HOUR_LEN = 55; // how far forwards does hand go? + const MIN_LEN = 72; + const HOUR_BACK = 10; // how far backwards dows hand go? + const MIN_BACK = 10; + const HOUR_W = 10; // width of cleared area + const MIN_W = 8; + + const get_hand = function(len, w, cornerw, overhang) { + return new Int8Array([ + 0, overhang+w, + -cornerw, overhang+cornerw, + -w, overhang, + -w, -len, + -cornerw, -len - cornerw, + 0, -len - w, + cornerw, -len - cornerw, + w, -len, + w, overhang, + cornerw, overhang+cornerw + ]); + }; + const hand_hour = get_hand(HOUR_LEN, 6, 4, HOUR_BACK); + const hand_hour_bg = get_hand(HOUR_LEN, HOUR_W, 8, HOUR_BACK); + const hand_minute = get_hand(MIN_LEN, 4, 3, MIN_BACK); + const hand_minute_bg = get_hand(MIN_LEN, MIN_W, 6, MIN_BACK); + + + // schedule a draw for the next minute + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + // draw the clock hands + let drawHands = function() { + let h = (date.getHours() + date.getMinutes()/60)*Math.PI/6, m = date.getMinutes()*Math.PI/30; + g.setColor(g.theme.bg).fillPolyAA(g.transformVertices(hand_hour_bg,{x:W/2,y:H/2,rotate:h})); + g.fillPolyAA(g.transformVertices(hand_minute_bg,{x:W/2,y:H/2,rotate:m})); + g.setColor("#f00").fillPolyAA(g.transformVertices(hand_hour,{x:W/2,y:H/2,rotate:h})); + g.setColor(g.theme.fg).fillPolyAA(g.transformVertices(hand_minute,{x:W/2,y:H/2,rotate:m})); + }; + + // return the screen area covered by clock hands (used for filling in background) + let getHandBounds = function() { + let h = (date.getHours() + date.getMinutes()/60)*Math.PI/6, m = date.getMinutes()*Math.PI/30; + let sh = Math.sin(h), ch = Math.cos(h), sm = Math.sin(m), cm = Math.cos(m); + return { + x1 : Math.round((W/2)+Math.min(sh*HOUR_LEN, sm*MIN_LEN, -sh*HOUR_BACK, -sm*MIN_BACK)-HOUR_W), + y1 : Math.round((H/2)-Math.max(ch*HOUR_LEN, cm*MIN_LEN, -ch*HOUR_BACK, -cm*MIN_BACK)-HOUR_W), + x2 : Math.round((W/2)+Math.max(sh*HOUR_LEN, sm*MIN_LEN, -sh*HOUR_BACK, -sm*MIN_BACK)+HOUR_W), + y2 : Math.round((H/2)-Math.min(ch*HOUR_LEN, cm*MIN_LEN, -ch*HOUR_BACK, -cm*MIN_BACK)+HOUR_W), + }; + }; + + let draw = function() { + // queue next draw in one minute + queueDraw(); + // work out locale-friendly date/time + date = new Date(); + //var timeStr = require("locale").time(date,1); + //var dateStr = require("locale").date(date); + // fill in area that we changed last time + background.fillRect(lastModified.x1, lastModified.y1, lastModified.x2, lastModified.y2); + if (!lastModified.first) { // first draw we don't have clockInfoMenuA/etc defined + //print(lastModified); + if (lastModified.y1<40) { + if (lastModified.x1 < 40 || + (lastModified.x1 < W/2 && lastModified.y1 < 16)) clockInfoMenuA.redraw(); + if (lastModified.x2 > W-40 || + (lastModified.x1 > W/2 && lastModified.y1 < 16)) clockInfoMenuB.redraw(); + } + if (lastModified.y2>W-40) { + if (lastModified.x1 < 40 || + (lastModified.x1 < W/2 && lastModified.y2>W-16)) clockInfoMenuD.redraw(); + if (lastModified.x2 > W-40 || + (lastModified.x1 > W/2 && lastModified.y2>W-16)) clockInfoMenuC.redraw(); + } + } + // draw hands + drawHands(); + lastModified = getHandBounds(); + //g.drawRect(lastModified); // debug + }; + + // Clear the screen once, at startup + background.fillRect(0, 0, W - 1, H - 1); + // draw immediately at first, queue update + draw(); + + let clockInfoMenuA, clockInfoMenuB, clockInfoMenuC, clockInfoMenuD; + // Show launcher when middle button pressed + Bangle.setUI({ + mode: "clock", + redraw : draw, + remove: function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (clockInfoMenuA) clockInfoMenuA.remove(); + if (clockInfoMenuB) clockInfoMenuB.remove(); + if (clockInfoMenuC) clockInfoMenuC.remove(); + if (clockInfoMenuD) clockInfoMenuD.remove(); + require("widget_utils").show(); // re-show widgets + } + }); + // Load widgets + Bangle.loadWidgets(); + require("widget_utils").hide(); + + // render clockinfos + let clockInfoDraw = function(itm, info, options) { + // itm: the item containing name/hasRange/etc + // info: data returned from itm.get() containing text/img/etc + // options: options passed into addInteractive + const left = options.x < 88, + top = options.y < 88, + imgx = left ? 1 : W - 28, imgy = top ? 19 : H - 42, + textx = left ? 2 : W - 1, texty = top ? 2 : H - 16; + let bg = g.theme.bg, fg = g.theme.fg; + // Clear the background + g.reset(); + background.fillRect(imgx, imgy, imgx+25, imgy+25); // erase image + background.fillRect(left?0:W/2, texty-1, left?W/2:W-1, texty+15); // erase text + // indicate focus - change colours + if (options.focus) { + bg = g.theme.fg; + fg = g.toColor("#f00"); + } + + if (info.img) + require("clock_info").drawBorderedImage(info.img,imgx,imgy); + + g.setFont("6x8:2").setFontAlign(left ? -1 : 1, -1); + g.setColor(bg).drawString(info.text, textx-2, texty). // draw the text background + drawString(info.text, textx+2, texty). + drawString(info.text, textx, texty-2). + drawString(info.text, textx, texty+2); + g.setColor(fg).drawString(info.text, textx, texty); // draw the text + // redraw hands if needed + if ((top && lastModified.x1=texty)) { + g.reset(); + drawHands(); + } + }; + + // Load the clock infos + let clockInfoItems = require("clock_info").load(); + let clockInfoItemsBangle = clockInfoItems.find(i=>i.name=="Bangle"); + // Add extra Calendar and digital clock ClockInfos + if (clockInfoItemsBangle) { + if (!clockInfoItemsBangle.items.find(i=>i.name=="Date")) { + clockInfoItemsBangle.items.push({ name : "Date", + get : () => { + let d = new Date(); + let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0); + g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17); + return { + text : require("locale").dow(d,1).toUpperCase(), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 86400000); + }, 86400000 - (Date.now() % 86400000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + }); + } + if (!clockInfoItemsBangle.items.find(i=>i.name=="Clock")) { + clockInfoItemsBangle.items.push({ name : "Clock", + get : () => { + return { + text : require("locale").time(new Date(),1), + img : atob("GBiBAAAAAAB+AAD/AAD/AAH/gAP/wAP/wAYAYAYAYAYAYAYAYAYAcAYAcAYAYAYAYAYAYAYAYAP/wAP/wAH/gAD/AAD/AAB+AAAAAA==") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 60000); + }, 60000 - (Date.now() % 60000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + }); + } + } + + + // Add the 4 clockinfos + const CLOCKINFOSIZE = 50; + clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, { + x: 0, + y: 0, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, { + x: W - CLOCKINFOSIZE, + y: 0, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuC = require("clock_info").addInteractive(clockInfoItems, { + x: W - CLOCKINFOSIZE, + y: H - CLOCKINFOSIZE, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuD = require("clock_info").addInteractive(clockInfoItems, { + x: 0, + y: H - CLOCKINFOSIZE, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + + /*setInterval(function() { + date.ms += 60000; draw(); + }, 500);*/ + } \ No newline at end of file diff --git a/apps/analogquadclk/icon.png b/apps/analogquadclk/icon.png new file mode 100644 index 000000000..79ded13c1 Binary files /dev/null and b/apps/analogquadclk/icon.png differ diff --git a/apps/analogquadclk/metadata.json b/apps/analogquadclk/metadata.json new file mode 100644 index 000000000..d60255717 --- /dev/null +++ b/apps/analogquadclk/metadata.json @@ -0,0 +1,16 @@ +{ "id": "analogquadclk", + "name": "Analog Quad Clock", + "shortName":"Quad Clock", + "version":"0.03", + "description": "An analog clock with clockinfos in each of the 4 corners, allowing 4 different data types to be rendered at once", + "icon": "icon.png", + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ], + "type": "clock", + "tags": "clock,clkinfo,analog,clockbg", + "supports" : ["BANGLEJS2"], + "dependencies" : { "clock_info":"module", "clockbg":"module" }, + "storage": [ + {"name":"analogquadclk.app.js","url":"app.js"}, + {"name":"analogquadclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/analogquadclk/screenshot.png b/apps/analogquadclk/screenshot.png new file mode 100644 index 000000000..f1a1dd6b5 Binary files /dev/null and b/apps/analogquadclk/screenshot.png differ diff --git a/apps/analogquadclk/screenshot2.png b/apps/analogquadclk/screenshot2.png new file mode 100644 index 000000000..a62a94f58 Binary files /dev/null and b/apps/analogquadclk/screenshot2.png differ diff --git a/apps/andark/settings.js b/apps/andark/settings.js new file mode 100644 index 000000000..708913705 --- /dev/null +++ b/apps/andark/settings.js @@ -0,0 +1,28 @@ +(function(back) { + const defaultSettings = { + loadWidgets : false, + textAboveHands : false, + shortHrHand : false + } + let settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{}); + + const save = () => require('Storage').write('andark.json', settings); + + const appMenu = { + '': {title: 'andark'}, '< Back': back, + /*LANG*/'Load widgets': { + value : !!settings.loadWidgets, + onchange : v => { settings.loadWidgets=v; save();} + }, + /*LANG*/'Text above hands': { + value : !!settings.textAboveHands, + onchange : v => { settings.textAboveHands=v; save();} + }, + /*LANG*/'Short hour hand': { + value : !!settings.shortHrHand, + onchange : v => { settings.shortHrHand=v; save();} + }, + }; + + E.showMenu(appMenu); +}); diff --git a/apps/android/test.json b/apps/android/test.json new file mode 100644 index 000000000..429fd70fe --- /dev/null +++ b/apps/android/test.json @@ -0,0 +1,99 @@ +{ + "app" : "android", + "setup" : [{ + "id": "default", + "steps" : [ + {"t":"cmd", "js": "Bangle.setGPSPower=(isOn, appID)=>{if (!appID) appID='?';if (!Bangle._PWR) Bangle._PWR={};if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);return Bangle._PWR.GPS.length>0;};", "text": "Fake the setGPSPower"}, + {"t":"wrap", "fn": "Bangle.setGPSPower", "id": "gpspower"}, + {"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port println"}, + {"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"}, + {"t":"cmd", "js": "Bangle._PWR={}", "text": "Prepare an empty _PWR for following asserts"}, + {"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"}, + {"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"} + ] + },{ + "id": "connected", + "steps" : [ + {"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: true };}", "text": "Control the security status to be connected"} + ] + },{ + "id": "disconnected", + "steps" : [ + {"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: false };}", "text": "Control the security status to be disconnected"} + ] + }], + "tests" : [{ + "description": "Check setGPSPower is replaced", + "steps" : [ + {"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port"}, + {"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"}, + {"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"}, + {"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"}, + {"t":"assert", "js": "Bangle.setGPSPower.toString().includes('native')", "is":"false", "text": "setGPSPower has been replaced"} + ] + },{ + "description": "Test switching hardware GPS on and off", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ] , "text": "internal GPS switched on"}, + {"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns falsy when switching off"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + {"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ] , "text": "internal GPS switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, internal GPS active until GB GPS event arrives", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on"}, + + {"t":"gb", "obj":{"t":"gps"}}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients still there"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn still shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, internal stays off", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + + {"t":"gb", "obj":{"t":"gps"}}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"}, + + {"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS still switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, but no event arrives", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + + {"t":"resetCall", "id": "gpspower"}, + {"t":"gb", "obj":{"t":"gps"}, "text": "trigger switch"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"}, + + {"t":"resetCall", "id": "gpspower"}, + {"t":"advanceTimers", "ms":"12000", "text": "wait for fallback"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on caused by missing GB event"} + ] + }] +} diff --git a/apps/antonclk/test.json b/apps/antonclk/test.json new file mode 100644 index 000000000..a719e0a14 --- /dev/null +++ b/apps/antonclk/test.json @@ -0,0 +1,15 @@ +{ + "app" : "antonclk", + "tests" : [{ + "description": "Check memory usage after setUI", + "steps" : [ + {"t":"cmd", "js": "Bangle.loadWidgets()"}, + {"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {"t":"cmd", "js": "Bangle.setUI()"}, + {"t":"saveMemoryUsage"}, + {"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {"t":"cmd", "js":"Bangle.setUI()"}, + {"t":"checkMemoryUsage"} + ] + }] +} diff --git a/apps/autoreset/settings.js b/apps/autoreset/settings.js new file mode 100644 index 000000000..8cbccd6f0 --- /dev/null +++ b/apps/autoreset/settings.js @@ -0,0 +1,115 @@ +(function(back) { + var FILE = 'autoreset.json'; + // Mode can be 'blacklist' or 'whitelist' + // Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode + var DEFAULTS = { + 'mode': 0, + 'apps': [], + 'timeout': 10 + }; + + var settings = {}; + + var loadSettings = function() { + settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; + }; + + var saveSettings = function(settings) { + require('Storage').write(FILE, settings); + }; + + // Get all app info files + var getApps = function() { + var apps = require('Storage').list(/\.info$/).map(appInfoFileName => { + var appInfo = require('Storage').readJSON(appInfoFileName, 1); + return appInfo && { + 'name': appInfo.name, + 'sortorder': appInfo.sortorder, + 'src': appInfo.src, + 'files': appInfo.files + }; + }).filter(app => app && !!app.src); + apps.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + return apps; + }; + + var showMenu = function() { + var menu = { + '': { 'title': 'Auto Reset' }, + /*LANG*/'< Back': () => { + back(); + }, + /*LANG*/'Mode': { + value: settings.mode, + min: 0, + max: 1, + format: v => ["Blacklist", "Whitelist"][v], + onchange: v => { + settings.mode = v; + saveSettings(settings); + }, + }, + /*LANG*/'App List': () => { + showAppSubMenu(); + }, + /*LANG*/'Timeout [min]': { + value: settings.timeout, + min: 0.25, max: 30, step : 0.25, + format: v => v, + onchange: v => { + settings.timeout = v; + saveSettings(settings); + }, + }, + }; + + E.showMenu(menu); + }; + + var showAppSubMenu = function() { + var menu = { + '': { 'title': 'Auto Reset' }, + '< Back': () => { + showMenu(); + }, + 'Add App': () => { + showAppList(); + } + }; + settings.apps.forEach(app => { + menu[app.name] = () => { + settings.apps.splice(settings.apps.indexOf(app), 1); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + var showAppList = function() { + var apps = getApps(); + var menu = { + '': { 'title': 'Auto Reset' }, + /*LANG*/'< Back': () => { + showMenu(); + } + }; + apps.forEach(app => { + menu[app.name] = () => { + settings.apps.push(app); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) diff --git a/apps/blecsc/ChangeLog b/apps/blecsc/ChangeLog new file mode 100644 index 000000000..1500000b5 --- /dev/null +++ b/apps/blecsc/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version +0.02: Minor code improvements +0.03: Moved from cycling app, fixed connection issues and cadence +0.04: Added support for <1 wheel/crank event/second (using idle counters) (ref #3434) +0.05: Fix <1 event/second issue \ No newline at end of file diff --git a/apps/blecsc/README.md b/apps/blecsc/README.md new file mode 100644 index 000000000..5cde87168 --- /dev/null +++ b/apps/blecsc/README.md @@ -0,0 +1,32 @@ +# BLE Cycling Speed Sencor (CSC) + +Displays data from a BLE Cycling Speed and Cadence sensor. + +Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over. + +## Settings + +Accessible from `Settings -> Apps -> BLE CSC` + +Here you can set the wheel diameter + +## Development + +``` +var csc = require("blecsc").getInstance(); +csc.on("status", txt => { + print("##", txt); + E.showMessage(txt); +}); +csc.on("data", e => print(e)); +csc.start(); +``` + +The `data` event contains: + + * cwr/ccr => wheel/crank cumulative revs + * lwet/lcet => wheel/crank last event time in 1/1024s + * wrps/crps => calculated wheel/crank revs per second + * wdt/cdt => time period in seconds between events + * wr => wheel revs + * kph => kilometers per hour \ No newline at end of file diff --git a/apps/blecsc/blecsc.js b/apps/blecsc/blecsc.js new file mode 100644 index 000000000..9b7d8b751 --- /dev/null +++ b/apps/blecsc/blecsc.js @@ -0,0 +1,230 @@ +/** + * This library communicates with a Bluetooth CSC peripherial using the Espruino NRF library. + * + * ## Usage: + * 1. Register event handlers using the \`on(eventName, handlerFunction)\` method + * You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can + * have raw characteristic values passed through using the \`value\` event. + * 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method + * 3. To tear down the connection, call the \`disconnect()\` method + * + * ## Events + * - \`status\` - string containing connection status + * - \`data\` - the peripheral sends a notification containing wheel/crank event data + * - \`disconnect\` - the peripheral ends the connection or the connection is lost + * + * cwr/ccr => wheel/crank cumulative revs + * lwet/lcet => wheel/crank last event time in 1/1024s + * wrps/crps => calculated wheel/crank revs per second + * wdt/cdt => time period in seconds between events + * wr => wheel revs + * kph => kilometers per hour + */ +class BLECSC { + constructor() { + this.reconnect = false; // set when start called + this.device = undefined; // set when device found + this.gatt = undefined; // set when connected + // .on("status", => string + // .on("data" + // .on("disconnect" + this.resetStats(); + // Set default values and merge with stored values + this.settings = Object.assign({ + circum: 2068 // circumference in mm + }, (require('Storage').readJSON('blecsc.json', true) || {})); + } + + resetStats() { + this.cwr = undefined; + this.ccr = undefined; + this.lwet = undefined; + this.lcet = undefined; + this.lastCwr = undefined; + this.lastCcr = undefined; + this.lastLwet = undefined; + this.lastLcet = undefined; + this.kph = undefined; + this.wrps = 0; // wheel revs per second + this.crps = 0; // crank revs per second + this.widle = 0; // wheel idle counter + this.cidle = 0; // crank idle counter + //this.batteryLevel = undefined; + } + + getDeviceAddress() { + if (!this.device || !this.device.id) + return '00:00:00:00:00:00'; + return this.device.id.split(" ")[0]; + } + + status(txt) { + this.emit("status", txt); + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + this.status("Scanning"); + // Find a device, then get the CSC Service and subscribe to + // notifications on the CSC Measurement characteristic. + // NRF.setLowPowerConnection(true); + var reconnect = this.reconnect; // auto-reconnect + return NRF.requestDevice({ + timeout: 5000, + filters: [{ + services: ["1816"] + }], + }).then(device => { + this.status("Connecting"); + this.device = device; + this.device.on('gattserverdisconnected', event => { + this.device = undefined; + this.gatt = undefined; + this.resetStats(); + this.status("Disconnected"); + this.emit("disconnect", event); + if (reconnect) {// auto-reconnect + reconnect = false; + setTimeout(() => { + if (this.reconnect) this.connect().then(() => {}, () => {}); + }, 500); + } + }); + + return new Promise(resolve => setTimeout(resolve, 150)); // On CooSpo we get a 'Connection Timeout' if we try and connect too soon + }).then(() => { + return this.device.gatt.connect(); + }).then(gatt => { + this.status("Connected"); + this.gatt = gatt; + return gatt.getPrimaryService("1816"); + }).then(service => { + return service.getCharacteristic("2a5b"); // UUID of the CSC measurement characteristic + }).then(characteristic => { + // register for changes on 2a5b + characteristic.on('characteristicvaluechanged', event => { + const flags = event.target.value.getUint8(0); + var offs = 0; + var data = {}; + if (flags & 1) { // FLAGS_WREV_BM + this.lastCwr = this.cwr; + this.lastLwet = this.lwet; + this.cwr = event.target.value.getUint32(1, true); + this.lwet = event.target.value.getUint16(5, true); + if (this.lastCwr === undefined) this.lastCwr = this.cwr; + if (this.lastLwet === undefined) this.lastLwet = this.lwet; + if (this.lwet < this.lastLwet) this.lastLwet -= 65536; + let secs = (this.lwet - this.lastLwet) / 1024; + if (secs) { + this.wrps = (this.cwr - this.lastCwr) / secs; + this.widle = 0; + } else { + if (this.widle<5) this.widle++; + else this.wrps = 0; + } + this.kph = this.wrps * this.settings.circum / 3600; + Object.assign(data, { // Notify the 'wheelEvent' handler + cwr: this.cwr, // cumulative wheel revolutions + lwet: this.lwet, // last wheel event time + wrps: this.wrps, // wheel revs per second + wr: this.cwr - this.lastCwr, // wheel revs + wdt : secs, // time period + kph : this.kph + }); + offs += 6; + } + if (flags & 2) { // FLAGS_CREV_BM + this.lastCcr = this.ccr; + this.lastLcet = this.lcet; + this.ccr = event.target.value.getUint16(offs + 1, true); + this.lcet = event.target.value.getUint16(offs + 3, true); + if (this.lastCcr === undefined) this.lastCcr = this.ccr; + if (this.lastLcet === undefined) this.lastLcet = this.lcet; + if (this.lcet < this.lastLcet) this.lastLcet -= 65536; + let secs = (this.lcet - this.lastLcet) / 1024; + if (secs) { + this.crps = (this.ccr - this.lastCcr) / secs; + this.cidle = 0; + } else { + if (this.cidle<5) this.cidle++; + else this.crps = 0; + } + Object.assign(data, { // Notify the 'crankEvent' handler + ccr: this.ccr, // cumulative crank revolutions + lcet: this.lcet, // last crank event time + crps: this.crps, // crank revs per second + cdt : secs, // time period + }); + } + this.emit("data",data); + }); + return characteristic.startNotifications(); +/* }).then(() => { + return this.gatt.getPrimaryService("180f"); + }).then(service => { + return service.getCharacteristic("2a19"); + }).then(characteristic => { + characteristic.on('characteristicvaluechanged', (event)=>{ + this.batteryLevel = event.target.value.getUint8(0); + }); + return characteristic.startNotifications();*/ + }).then(() => { + this.status("Ready"); + }, err => { + this.status("Error: " + err); + if (reconnect) { // auto-reconnect + reconnect = false; + setTimeout(() => { + if (this.reconnect) this.connect().then(() => {}, () => {}); + }, 500); + } + throw err; + }); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (!this.gatt) return; + this.gatt.disconnect(); + this.gatt = undefined; + } + + /* Start trying to connect - will keep searching and attempting to connect*/ + start() { + this.reconnect = true; + if (!this.device) + this.connect().then(() => {}, () => {}); + } + + /* Stop trying to connect, and disconnect */ + stop() { + this.reconnect = false; + this.disconnect(); + } +} + +// Get an instance of BLECSC or create one if it doesn't exist +BLECSC.getInstance = function() { + if (!BLECSC.instance) { + BLECSC.instance = new BLECSC(); + } + return BLECSC.instance; +}; + +exports = BLECSC; + +/* +var csc = require("blecsc").getInstance(); +csc.on("status", txt => { + print("##", txt); + E.showMessage(txt); +}); +csc.on("data", e => print(e)); +csc.start(); +*/ diff --git a/apps/blecsc/clkinfo.js b/apps/blecsc/clkinfo.js new file mode 100644 index 000000000..9a9515c3a --- /dev/null +++ b/apps/blecsc/clkinfo.js @@ -0,0 +1,74 @@ +(function() { + var csc = require("blecsc").getInstance(); + //csc.on("status", txt => { print("CSC",txt); }); + csc.on("data", e => { + ci.items.forEach(it => { if (it._visible) it.emit('redraw'); }); + }); + csc.on("disconnect", e => { + // redraw all with no info + ci.items.forEach(it => { if (it._visible) it.emit('redraw'); }); + }); + var uses = 0; + var ci = { + name: "CSC", + items: [ + { name : "Speed", + get : () => { + return { + text : (csc.kph === undefined) ? "--" : require("locale").speed(csc.kph), + img : atob("GBiBAAAAAAAAAAAAAAABwAABwAeBgAMBgAH/gAH/wAPDwA/DcD9m/Ge35sW9o8//M8/7E8CBA2GBhn8A/h4AeAAAAAAAAAAAAAAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + }, + { name : "Distance", + get : () => { + return { + text : (csc.kph === undefined) ? "--" : require("locale").distance(csc.cwr * csc.settings.circum / 1000), + img : atob("GBiBAAAAAB8AADuAAGDAAGTAAGRAAEBAAGBAAGDAADCAADGAIB8B+A/BjAfjBgAyJgAyIgAyAj/jBnADBmABjGAA2HAA8D//4AAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + }, + { name : "Cadence", + get : () => { + return { + text : (csc.crps === undefined) ? "--" : Math.round(csc.crps*60), + img : atob("GBiBAAAAAAAAAAB+EAH/sAeB8A4A8AwB8BgAABgAADAAADAAADAAADAADDAADDAAABgAABgAGAwAEA4AAAeAwAH8gAB8AAAAAAAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + } + ] + }; + return ci; +}) + + diff --git a/apps/blecsc/icons8-cycling-48.png b/apps/blecsc/icons8-cycling-48.png new file mode 100644 index 000000000..0bc83859f Binary files /dev/null and b/apps/blecsc/icons8-cycling-48.png differ diff --git a/apps/blecsc/metadata.json b/apps/blecsc/metadata.json new file mode 100644 index 000000000..0daa01fc8 --- /dev/null +++ b/apps/blecsc/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "blecsc", + "name": "BLE Cycling Speed Sensor Library", + "shortName": "BLE CSC", + "version": "0.05", + "description": "Module to get live values from a BLE Cycle Speed (CSC) sensor. Includes recorder and clockinfo plugins", + "icon": "icons8-cycling-48.png", + "tags": "outdoors,exercise,ble,bluetooth,clkinfo", + "type":"module", + "provides_modules" : ["blecsc"], + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"blecsc","url":"blecsc.js"}, + {"name":"blecsc.settings.js","url":"settings.js"}, + {"name":"blecsc.recorder.js","url":"recorder.js"}, + {"name":"blecsc.clkinfo.js","url":"clkinfo.js"} + ], + "data": [ + {"name":"blecsc.json"} + ] +} diff --git a/apps/blecsc/recorder.js b/apps/blecsc/recorder.js new file mode 100644 index 000000000..510f50c3f --- /dev/null +++ b/apps/blecsc/recorder.js @@ -0,0 +1,28 @@ +(function(recorders) { + recorders.blecsc = function() { + var csc = require("blecsc").getInstance(); + var speed, cadence; + csc.on("data", e => { + speed = e.kph; // speed in KPH + cadence = (e.crps===undefined)?"":Math.round(e.crps*60); // crank rotations per minute + }); + return { + name : "CSC", + fields : ["Speed (kph)","Cadence (rpm)"], + getValues : () => { + var r = [speed,cadence]; + speed = ""; + cadence = ""; + return r; + }, + start : () => { + csc.start(); + }, + stop : () => { + csc.stop(); + }, + draw : (x,y) => g.setColor(csc.device?"#0f0":"#8f8").drawImage(atob("Dw+BAAAAAAABgOIA5gHcBxw9fpfTPqYRC8HgAAAAAAAA"),x,y) + }; + } +}) + diff --git a/apps/blecsc/settings.js b/apps/blecsc/settings.js new file mode 100644 index 000000000..b445b2541 --- /dev/null +++ b/apps/blecsc/settings.js @@ -0,0 +1,85 @@ +(function(back) { + const storage = require('Storage') + const SETTINGS_FILE = 'blecsc.json' + + // Set default values and merge with stored values + let settings = Object.assign({ + circum: 2068 // circumference in mm + }, (storage.readJSON(SETTINGS_FILE, true) || {})); + + function saveSettings() { + storage.writeJSON(SETTINGS_FILE, settings); + } + + function circumMenu() { + var v = 0|settings.circum; + var cm = 0|(v/10); + var mm = v-(cm*10); + E.showMenu({ + '': { title: /*LANG*/"Circumference", back: mainMenu }, + 'cm': { + value: cm, + min: 80, max: 240, step: 1, + onchange: (v) => { + cm = v; + settings.circum = (cm*10)+mm; + saveSettings(); + }, + }, + '+ mm': { + value: mm, + min: 0, max: 9, step: 1, + onchange: (v) => { + mm = v; + settings.circum = (cm*10)+mm; + saveSettings(); + }, + }, + /*LANG*/'Std Wheels': function() { + // https://support.wahoofitness.com/hc/en-us/articles/115000738484-Tire-Size-Wheel-Circumference-Chart + E.showMenu({ + '': { title: /*LANG*/'Std Wheels', back: circumMenu }, + '650x38 wheel' : function() { + settings.circum = 1995; + saveSettings(); + mainMenu(); + }, + '700x32c wheel' : function() { + settings.circum = 2152; + saveSettings(); + mainMenu(); + }, + '24"x1.75 wheel' : function() { + settings.circum = 1890; + saveSettings(); + mainMenu(); + }, + '26"x1.5 wheel' : function() { + settings.circum = 2010; + saveSettings(); + mainMenu(); + }, + '27.5"x1.5 wheel' : function() { + settings.circum = 2079; + saveSettings(); + mainMenu(); + } + }); + } + + }); + } + + function mainMenu() { + E.showMenu({ + '': { 'title': 'BLE CSC' }, + '< Back': back, + /*LANG*/'Circumference': { + value: settings.circum+"mm", + onchange: circumMenu + }, + }); + } + + mainMenu(); +}) \ No newline at end of file diff --git a/apps/bootbthomebatt/ChangeLog b/apps/bootbthomebatt/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/bootbthomebatt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/bootbthomebatt/README.md b/apps/bootbthomebatt/README.md new file mode 100644 index 000000000..79c379f33 --- /dev/null +++ b/apps/bootbthomebatt/README.md @@ -0,0 +1,11 @@ +# BLE BTHome Battery Service + +Broadcasts battery remaining percentage over BLE using the [BTHome protocol](https://bthome.io/) - which makes for easy integration into [Home Assistant](https://www.home-assistant.io/) + +## Usage + +This boot code runs in the background and has no user interface. + +## Creator + +[Deirdre O'Byrne](https://github.com/deirdreobyrne) diff --git a/apps/bootbthomebatt/bluetooth.png b/apps/bootbthomebatt/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/bootbthomebatt/bluetooth.png differ diff --git a/apps/bootbthomebatt/boot.js b/apps/bootbthomebatt/boot.js new file mode 100644 index 000000000..a9bdcd4d3 --- /dev/null +++ b/apps/bootbthomebatt/boot.js @@ -0,0 +1,14 @@ +var btHomeBatterySequence = 0; + +function advertiseBTHomeBattery() { + var advert = [0x40, 0x00, btHomeBatterySequence, 0x01, E.getBattery()]; + + require("ble_advert").set(0xFCD2, advert); + btHomeBatterySequence = (btHomeBatterySequence + 1) & 255; +} + +setInterval(function() { + advertiseBTHomeBattery(); +}, 300000); // update every 5 min + +advertiseBTHomeBattery(); diff --git a/apps/bootbthomebatt/metadata.json b/apps/bootbthomebatt/metadata.json new file mode 100644 index 000000000..0992edc24 --- /dev/null +++ b/apps/bootbthomebatt/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "bootbthomebatt", + "name": "BLE BTHome Battery Service", + "shortName": "BTHome Battery Service", + "version": "0.01", + "description": "Broadcasts battery remaining over bluetooth using the BTHome protocol - makes for easy integration with Home Assistant.\n", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "battery,ble,bluetooth,bthome", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"bthomebat.boot.js","url":"boot.js"} + ] +} diff --git a/apps/burn/.gitignore b/apps/burn/.gitignore new file mode 100644 index 000000000..656b79624 --- /dev/null +++ b/apps/burn/.gitignore @@ -0,0 +1 @@ +.prettierignore \ No newline at end of file diff --git a/apps/burn/ChangeLog b/apps/burn/ChangeLog new file mode 100644 index 000000000..66c4f98bd --- /dev/null +++ b/apps/burn/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App! +0.02: Added README.md +0.03: Icon update +0.04: Icon Fix +0.05: Misc cleanup for PR +0.06: Implementing fixes from PR comments +0.07: Bug fix diff --git a/apps/burn/README.md b/apps/burn/README.md new file mode 100644 index 000000000..44f1e260f --- /dev/null +++ b/apps/burn/README.md @@ -0,0 +1,30 @@ +# Burn: Calorie Counter + +Burn is a simple calorie counter application. It is based on the original Counter app and has been enhanced with additional features (I recommend using +it with the "Digital Clock Widget", if you intend to keep it running). + +## Features + +- **Persistent counter**: The counter value is saved to flash, so it persists even when the app is closed or the device is restarted. +- **Daily reset**: The counter resets each day, allowing you to track your calorie intake on a daily basis. +- **Adjustable increment value**: You can adjust the increment value to suit your needs. + +## Controls + +### Bangle.js 1 + +- **BTN1**: Increase (or tap right) +- **BTN3**: Decrease (or tap left) +- **Press BTN2**: Change increment + +### Bangle.js 2 + +- **Swipe up**: Increase +- **Swipe down**: Decrease +- **Press BTN**: Change increment + +## How it Works + +The counter value and the date are stored in a file named "kcal.txt". The counter value is read from the file when the app starts and written to the file whenever the counter value is updated. + +The app uses the current date to determine whether to reset the counter. If the date has changed since the last time the counter was updated, the counter is reset to 0. diff --git a/apps/burn/app-icon.js b/apps/burn/app-icon.js new file mode 100644 index 000000000..8cd3b7ca1 --- /dev/null +++ b/apps/burn/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/gUA///j32o8h9v6glA+P9+/3tu27YCLvICBgEGCJlFmwRBgALFxQRIgIdF28JmIIEknG7cMyVJk4nBC4PJk4dBC4OJkmSrYRCkuACQM26/88wRGgQHB2iGCm33//8GoXtonbraYGgwRB/+bNY4AEg9/CIPbth3BxYRJn4RB/YRBgEUTwIRGne275IBCIQABjYRGrpCB+VK1gJDgYQFgXN23YQwIjEAA0WMwPQ0mSqgRK2QRBy6cBCJUFGIO12wjBpgRMlsAqmSqTOCAA0sCINogEIyVKCJdLoEAhQRNpQFCyVII5IRGqARQNZUECIcGyRLBPpPSCIQWBsCzK3VJoEB0mTCBUAitplEA0WYCJb7B1EBCYIAsjJjCknDpMkyUAmlKNwuEyEAgMSwwQBpNhAQM4CAcDkgRBe4ODogOB4MT0MldgcxCIXEyWAi3axNgykECIcBxIRBEwIRBYoK3BykGkw1DxPEyEZksSCIMEpcDjAIBGocbhMQiMzCIUyqALB5KmFhMghMk0VLQANYPoLeBCI0SRgOKJYOAgOSpihFCIMaCIOTgMk4ACBqVICIyTBKIMZkkAhpyBo4RHgOk4EZPAIjByVFNYYIBAoU2AoOwAQO5kmMf7QALpMg2VJ23Mn////8OIVThmUCIs27FvCIP+eQOSpUIyXAgFNEYfQCIX8P4OSp0MCIVZgEWFoNgrMktuwgdt23YOIQ")) \ No newline at end of file diff --git a/apps/burn/app-icon.png b/apps/burn/app-icon.png new file mode 100644 index 000000000..23d4a13e6 Binary files /dev/null and b/apps/burn/app-icon.png differ diff --git a/apps/burn/app-screenshot.png b/apps/burn/app-screenshot.png new file mode 100644 index 000000000..fef0e701e Binary files /dev/null and b/apps/burn/app-screenshot.png differ diff --git a/apps/burn/app.js b/apps/burn/app.js new file mode 100644 index 000000000..348a19153 --- /dev/null +++ b/apps/burn/app.js @@ -0,0 +1,243 @@ +/* + * Burn: Calories Counter for Bangle.js (Espruino). Based on the original Counter app. + * Features: + * - Persistent counter: saved to a file. + * - Daily reset: counter resets each day. + * - Adjustable increment value. + * + * Bangle.js 1 Controls: + * - BTN1: Increase (or tap right) + * - BTN3: Decrease (or tap left) + * - Press BTN2: Change increment + * + * Bangle.js 2 Controls: + * - Swipe up: Increase + * - Swipe down: Decrease + * - Press BTN: Change increment + */ + +// File variable to handle file operations +let file; + +// Check if the hardware version is Bangle.js 2 +const BANGLEJS2 = process.env.HWVERSION == 2; + +// Importing the Storage module for file operations +const Storage = require("Storage"); + +// File path for the counter data +const PATH = "kcal.txt"; + +// Function to get the current date as a string +function dayString() { + const date = new Date(); + // Month is 0-indexed, so we add 1 to get the correct month number + return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; +} + +// Counter object to keep track of the count and the date +let counter = { count: 0, date: dayString() }; + +// Function to read the counter from the file +function readCounterFromFile() { + try { + // Open the file in read mode + file = Storage.open(PATH, "r"); + let line = file.readLine(); + + // If the file has content, parse it and update the counter + if (line) { + let splitLine = line.trim().split(","); + counter = { count: parseInt(splitLine[0]), date: splitLine[1] }; + } + } catch (err) { + // If the file does not exist, the counter will remain 0 + } +} + +// Function to write the counter to the file +function writeCounterToFile() { + // Open the file in write mode + file = Storage.open(PATH, "w"); + // Write the counter and date to the file + file.write(counter.count.toString() + "," + counter.date + "\n"); +} + +// Function to reset the counter +function resetCounter() { + // Reset the counter to 0 and update the date + counter = { count: 0, date: dayString() }; +} + +// Function to update the counter value +function updateCounterValue(value) { + // Update the counter with the new value, ensuring it's not less than 0 + counter = { count: Math.max(0, value), date: dayString() }; +} + +// Function to update the counter +function updateCounter(value) { + // If the date has changed, reset the counter + if (counter.date != dayString()) { + resetCounter(); + } else { + // Otherwise, update the counter value + updateCounterValue(value); + } + + // Write the updated counter to the file + writeCounterToFile(); + // Update the screen with the new counter value + updateScreen(); +} + +// Function to set a watch on a button to update the counter when pressed +function counterButtonWatch(button, increment) { + setWatch( + () => { + // If the button is for incrementing, or the counter is greater than 0, update the counter + if (increment || counter.count > 0) { + updateCounter( + counter.count + (increment ? getInterval() : -getInterval()) + ); + // Update the screen with the new counter value + updateScreen(); + } + }, + button, + { repeat: true } + ); +} + +// Function to create interval functions +const createIntervalFunctions = function () { + // Array of intervals + const intervals = [50, 100, 200, 10]; + // Current location in the intervals array + let location = 0; + + // Function to get the current interval + const getInterval = function () { + return intervals[location]; + }; + + // Function to rotate the increment + const rotateIncrement = function () { + // Update the location to the next index in the intervals array, wrapping around if necessary + location = (location + 1) % intervals.length; + // Update the screen with the new increment + updateScreen(); + }; + + // Return the getInterval and rotateIncrement functions + return { getInterval, rotateIncrement }; +}; + +// Create the interval functions +const intervalFunctions = createIntervalFunctions(); +const getInterval = intervalFunctions.getInterval; +const rotateIncrement = intervalFunctions.rotateIncrement; + +// Function to update the screen +function updateScreen() { + // Clear the screen area for the counter + g.clearRect(0, 50, 250, BANGLEJS2 ? 130 : 150) + .setBgColor(g.theme.bg) + .setColor(g.theme.fg) + .setFont("Vector", 40) + .setFontAlign(0, 0) + // Draw the counter value + .drawString(Math.floor(counter.count), g.getWidth() / 2, 100) + .setFont("6x8") + // Clear the screen area for the increment + .clearRect(g.getWidth() / 2 - 50, 140, g.getWidth() / 2 + 50, 160) + // Draw the increment value + .drawString("Increment: " + getInterval(), g.getWidth() / 2, 150); + + // If the hardware version is Bangle.js 1, draw the increment and decrement buttons + if (!BANGLEJS2) { + g.drawString("-", 45, 100).drawString("+", 185, 100); + } +} + +// If the hardware version is Bangle.js 2, set up the drag handling and button watch + +let drag; + +if (BANGLEJS2) { + // Set up drag handling + Bangle.on("drag", (e) => { + // If this is the start of a drag, record the initial coordinates + if (!drag) { + drag = { x: e.x, y: e.y }; + return; + } + + // If the button is still being pressed, ignore this event + if (e.b) return; + + // Calculate the change in x and y from the start of the drag + const dx = e.x - drag.x; + const dy = e.y - drag.y; + // Reset the drag start coordinates + drag = null; + + // Determine if the drag is primarily horizontal or vertical + const isHorizontalDrag = Math.abs(dx) > Math.abs(dy) + 10; + const isVerticalDrag = Math.abs(dy) > Math.abs(dx) + 10; + + // If the drag is primarily horizontal, ignore it + if (isHorizontalDrag) { + return; + } + + // If the drag is primarily vertical, update the counter + if (isVerticalDrag) { + // If the drag is downwards and the counter is greater than 0, decrease the counter + if (dy > 0 && counter.count > 0) { + updateCounter(counter.count - getInterval()); + } else if (dy < 0) { + // If the drag is upwards, increase the counter + updateCounter(counter.count + getInterval()); + } + // Update the screen with the new counter value + updateScreen(); + } + }); + + // Set a watch on the button to rotate the increment when pressed + setWatch(rotateIncrement, BTN1, { repeat: true }); +} else { + // If the hardware version is Bangle.js 1, set up the button watches + + // Set watch on button to increase the counter + counterButtonWatch(BTN1, true); + counterButtonWatch(BTN5, true); // screen tap + // Set watch on button to decrease the counter + counterButtonWatch(BTN3, false); + counterButtonWatch(BTN4, false); // screen tap + + // Set a watch on button to rotate the increment when pressed + setWatch( + () => { + rotateIncrement(); + }, + BTN2, + { repeat: true } + ); +} + +// clear the screen +g.clear(); + +// Set the background and foreground colors +g.setBgColor(g.theme.bg).setColor(g.theme.fg); + +// Load and draw the widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Read the counter from the file +readCounterFromFile(); +// Update the screen with the counter value +updateScreen(); diff --git a/apps/burn/metadata.json b/apps/burn/metadata.json new file mode 100644 index 000000000..c032058c8 --- /dev/null +++ b/apps/burn/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "burn", + "name": "Burn", + "version": "0.07", + "description": "Simple Calorie Counter -- saves to flash and resets at midnight. I often keep mine running while the digital clock widget is at the top", + "icon": "app-icon.png", + "tags": "tool", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "screenshots": [{"url":"app-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"burn.app.js","url":"app.js"}, + {"name":"burn.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/cards/EAN.js b/apps/cards/EAN.js new file mode 100644 index 000000000..177874494 --- /dev/null +++ b/apps/cards/EAN.js @@ -0,0 +1,73 @@ +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); + +// Standard start end and middle bits +const SIDE_BIN = '101'; +const MIDDLE_BIN = '01010'; + +// Base class for EAN8 & EAN13 +class EAN extends Barcode { + constructor(data, options) { + super(data, options); + } + + leftText(from, to) { + return this.text.substr(from, to); + } + + leftEncode(data, structure) { + return encode(data, structure); + } + + rightText(from, to) { + return this.text.substr(from, to); + } + + rightEncode(data, structure) { + return encode(data, structure); + } + + encode() { + const data = [ + SIDE_BIN, + this.leftEncode(), + MIDDLE_BIN, + this.rightEncode(), + SIDE_BIN + ]; + + return { + data: data.join(''), + text: this.text + }; + } + +} + +module.exports = EAN; \ No newline at end of file diff --git a/apps/cards/EAN13.js b/apps/cards/EAN13.js new file mode 100644 index 000000000..c91e385e3 --- /dev/null +++ b/apps/cards/EAN13.js @@ -0,0 +1,92 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Binary_encoding_of_data_digits_into_EAN-13_barcode +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const EAN = require("cards.EAN.js"); + +const EAN13_STRUCTURE = [ + 'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', + 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL' +]; + +// Calculate the checksum digit +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit +const checksum = (number) => { + const res = number + .substr(0, 12) + .split('') + .map((n) => +n) + .reduce((sum, a, idx) => ( + idx % 2 ? sum + a * 3 : sum + a + ), 0); + + return (10 - (res % 10)) % 10; +}; + +class EAN13 extends EAN { + + constructor(data, options) { + // Add checksum if it does not exist + if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) { + data += checksum(data); + } + + super(data, options); + + // Adds a last character to the end of the barcode + this.lastChar = options.lastChar; + } + + valid() { + return ( + /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + +this.data[12] === checksum(this.data) + ); + } + + leftText() { + return super.leftText(1, 6); + } + + leftEncode() { + const data = this.data.substr(1, 6); + const structure = EAN13_STRUCTURE[this.data[0]]; + return super.leftEncode(data, structure); + } + + rightText() { + return super.rightText(7, 6); + } + + rightEncode() { + const data = this.data.substr(7, 6); + return super.rightEncode(data, 'RRRRRR'); + } + +} + +module.exports = EAN13; \ No newline at end of file diff --git a/apps/cards/EAN8.js b/apps/cards/EAN8.js new file mode 100644 index 000000000..382ee647a --- /dev/null +++ b/apps/cards/EAN8.js @@ -0,0 +1,82 @@ +// Encoding documentation: +// http://www.barcodeisland.com/ean8.phtml +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const EAN = require("cards.EAN.js"); + +// Calculate the checksum digit +const checksum = (number) => { + const res = number + .substr(0, 7) + .split('') + .map((n) => +n) + .reduce((sum, a, idx) => ( + idx % 2 ? sum + a : sum + a * 3 + ), 0); + + return (10 - (res % 10)) % 10; +}; + +class EAN8 extends EAN { + + constructor(data, options) { + // Add checksum if it does not exist + if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) { + data += checksum(data); + } + + super(data, options); + } + + valid() { + return ( + /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + +this.data[7] === checksum(this.data) + ); + } + + leftText() { + return super.leftText(0, 4); + } + + leftEncode() { + const data = this.data.substr(0, 4); + return super.leftEncode(data, 'LLLL'); + } + + rightText() { + return super.rightText(4, 4); + } + + rightEncode() { + const data = this.data.substr(4, 4); + return super.rightEncode(data, 'RRRR'); + } + +} + +module.exports = EAN8; diff --git a/apps/cards/UPC.js b/apps/cards/UPC.js new file mode 100644 index 000000000..6f581ca18 --- /dev/null +++ b/apps/cards/UPC.js @@ -0,0 +1,79 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); + +class UPC extends Barcode{ + constructor(data, options){ + // Add checksum if it does not exist + if(/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + data += checksum(data); + } + + super(data, options); + } + + valid(){ + return /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + this.data[11] == checksum(this.data); + } + + encode(){ + var result = ""; + + result += "101"; + result += encode(this.data.substr(0, 6), "LLLLLL"); + result += "01010"; + result += encode(this.data.substr(6, 6), "RRRRRR"); + result += "101"; + + return { + data: result, + text: this.text + }; + } +} + +// Calulate the checksum digit +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit +function checksum(number){ + var result = 0; + + var i; + for(i = 1; i < 11; i += 2){ + result += parseInt(number[i]); + } + for(i = 0; i < 11; i += 2){ + result += parseInt(number[i]) * 3; + } + + return (10 - (result % 10)) % 10; +} + +module.exports = { UPC, checksum }; diff --git a/apps/cards/UPCE.js b/apps/cards/UPCE.js new file mode 100644 index 000000000..9aaa464b7 --- /dev/null +++ b/apps/cards/UPCE.js @@ -0,0 +1,134 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding +// +// UPC-E documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); +const upc = require("cards.UPC.js"); + +const EXPANSIONS = [ + "XX00000XXX", + "XX10000XXX", + "XX20000XXX", + "XXX00000XX", + "XXXX00000X", + "XXXXX00005", + "XXXXX00006", + "XXXXX00007", + "XXXXX00008", + "XXXXX00009" +]; + +const PARITIES = [ + ["EEEOOO", "OOOEEE"], + ["EEOEOO", "OOEOEE"], + ["EEOOEO", "OOEEOE"], + ["EEOOOE", "OOEEEO"], + ["EOEEOO", "OEOOEE"], + ["EOOEEO", "OEEOOE"], + ["EOOOEE", "OEEEOO"], + ["EOEOEO", "OEOEOE"], + ["EOEOOE", "OEOEEO"], + ["EOOEOE", "OEEOEO"] +]; + +class UPCE extends Barcode{ + constructor(data, options){ + // Code may be 6 or 8 digits; + // A 7 digit code is ambiguous as to whether the extra digit + // is a UPC-A check or number system digit. + super(data, options); + this.isValid = false; + if(/^[0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + this.middleDigits = data; + this.upcA = expandToUPCA(data, "0"); + this.text = options.text || + `${this.upcA[0]}${data}${this.upcA[this.upcA.length - 1]}`; + this.isValid = true; + } + else if(/^[01][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + this.middleDigits = data.substring(1, data.length - 1); + this.upcA = expandToUPCA(this.middleDigits, data[0]); + + if(this.upcA[this.upcA.length - 1] === data[data.length - 1]){ + this.isValid = true; + } + else{ + // checksum mismatch + return; + } + } + } + + valid(){ + return this.isValid; + } + + encode(){ + var result = ""; + + result += "101"; + result += this.encodeMiddleDigits(); + result += "010101"; + + return { + data: result, + text: this.text + }; + } + + encodeMiddleDigits() { + const numberSystem = this.upcA[0]; + const checkDigit = this.upcA[this.upcA.length - 1]; + const parity = PARITIES[parseInt(checkDigit)][parseInt(numberSystem)]; + return encode(this.middleDigits, parity); + } +} + +function expandToUPCA(middleDigits, numberSystem) { + const lastUpcE = parseInt(middleDigits[middleDigits.length - 1]); + const expansion = EXPANSIONS[lastUpcE]; + + let result = ""; + let digitIndex = 0; + for(let i = 0; i < expansion.length; i++) { + let c = expansion[i]; + if (c === 'X') { + result += middleDigits[digitIndex++]; + } else { + result += c; + } + } + + result = `${numberSystem}${result}`; + return `${result}${upc.checksum(result)}`; +} + +module.exports = UPCE; \ No newline at end of file diff --git a/apps/cards/encode.js b/apps/cards/encode.js new file mode 100644 index 000000000..7cb1fcc87 --- /dev/null +++ b/apps/cards/encode.js @@ -0,0 +1,67 @@ +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const BINARIES = { + 'L': [ // The L (left) type of encoding + '0001101', '0011001', '0010011', '0111101', '0100011', + '0110001', '0101111', '0111011', '0110111', '0001011' + ], + 'G': [ // The G type of encoding + '0100111', '0110011', '0011011', '0100001', '0011101', + '0111001', '0000101', '0010001', '0001001', '0010111' + ], + 'R': [ // The R (right) type of encoding + '1110010', '1100110', '1101100', '1000010', '1011100', + '1001110', '1010000', '1000100', '1001000', '1110100' + ], + 'O': [ // The O (odd) encoding for UPC-E + '0001101', '0011001', '0010011', '0111101', '0100011', + '0110001', '0101111', '0111011', '0110111', '0001011' + ], + 'E': [ // The E (even) encoding for UPC-E + '0100111', '0110011', '0011011', '0100001', '0011101', + '0111001', '0000101', '0010001', '0001001', '0010111' + ] +}; + +// Encode data string +const encode = (data, structure, separator) => { + let encoded = data + .split('') + .map((val, idx) => BINARIES[structure[idx]]) + .map((val, idx) => val ? val[data[idx]] : ''); + + if (separator) { + const last = data.length - 1; + encoded = encoded.map((val, idx) => ( + idx < last ? val + separator : val + )); + } + + return encoded.join(''); +}; + +module.exports = encode; \ No newline at end of file diff --git a/apps/chronlog/ChangeLog b/apps/chronlog/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/chronlog/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/chronlog/README.md b/apps/chronlog/README.md new file mode 100644 index 000000000..7c542cf73 --- /dev/null +++ b/apps/chronlog/README.md @@ -0,0 +1,53 @@ +# Chrono Logger + +Record times active on a task, course, work or anything really. + +**Disclaimer:** No one is responsible for any loss of data you recorded with this app. If you run into problems please report as advised under **Requests** below. + +With time on your side and a little help from your friends - you'll surely triumph over Lavos in the end! + +![dump](dump.png) ![dump1](dump1.png) ![dump2](dump2.png) ![dump3](dump3.png) ![dump4](dump4.png) ![dump5](dump5.png) ![dump6](dump6.png) + + +## Usage + +Click the large green button to log the start of your activity. Click the now red button again to log that you stopped. + +## Features + +- Saves to file on every toggling of the active state. + - csv file contents looks like: + ``` + 1,Start,2024-03-02T15:18:09 GMT+0200 + 2,Note,Critical hit! + 3,Stop,2024-03-02T15:19:17 GMT+0200 + ``` +- Add annotations to the log. +- Create and switch between multiple logs. +- Sync log files to an Android device through Gadgetbridge (Needs pending code changes to Gadgetbridge). +- App state is restored when you start the app again. + +## Controls + +- Large button to toggle active state. +- Menu icon to access additional functionality. +- Hardware button exits menus, closes the app on the main screen. + +## TODO and notes + +- Delete individual tasks/logs through the app? +- Reset everything through the app? +- Scan for chronlog storage files that somehow no longer have tasks associated with it? +- Complete the Gadgetbridge side of things for sync. +- Sync to iOS? +- Inspect log files through the app, similarly to Recorder app? +- Changes to Android file system permissions makes it not always trivial to access the synced files. + + +## Requests + +Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions. + +## Creator + +[thyttan](https://github.com/thyttan) diff --git a/apps/chronlog/app-icon.js b/apps/chronlog/app-icon.js new file mode 100644 index 000000000..dc25e4b5b --- /dev/null +++ b/apps/chronlog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA=")) diff --git a/apps/chronlog/app.js b/apps/chronlog/app.js new file mode 100644 index 000000000..827ca14e1 --- /dev/null +++ b/apps/chronlog/app.js @@ -0,0 +1,376 @@ +// TODO: +// - Add more /*LANG*/ tags for translations. +// - Check if there are chronlog storage files that should be added to tasks. + +{ + const storage = require("Storage"); + let appData = storage.readJSON("chronlog.json", true) || { + currentTask : "default", + tasks : { + default: { + file : "chronlog_default.csv", // Existing default task log file + state : "stopped", + lineNumber : 0, + lastLine : "", + lastSyncedLine : "", + }, + // Add more tasks as needed + }, + }; + let currentTask = appData.currentTask; + let tasks = appData.tasks; + delete appData; + + let themeColors = g.theme; + + let logEntry; // Avoid previous lint warning + + // Function to draw the Start/Stop button with play and pause icons + let drawButton = ()=>{ + var btnWidth = g.getWidth() - 40; + var btnHeight = 50; + var btnX = 20; + var btnY = (g.getHeight() - btnHeight) / 2; + var cornerRadius = 25; + + var isStopped = tasks[currentTask].state === "stopped"; + g.setColor(isStopped ? "#0F0" : "#F00"); // Set color to green when stopped and red when started + + // Draw rounded corners of the button + g.fillCircle(btnX + cornerRadius, btnY + cornerRadius, cornerRadius); + g.fillCircle(btnX + btnWidth - cornerRadius, btnY + cornerRadius, cornerRadius); + g.fillCircle(btnX + cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius); + g.fillCircle(btnX + btnWidth - cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius); + + // Draw rectangles to fill in the button + g.fillRect(btnX + cornerRadius, btnY, btnX + btnWidth - cornerRadius, btnY + btnHeight); + g.fillRect(btnX, btnY + cornerRadius, btnX + btnWidth, btnY + btnHeight - cornerRadius); + + g.setColor(themeColors.bg); // Set icon color to contrast against the button's color + + // Center the icon within the button + var iconX = btnX + btnWidth / 2; + var iconY = btnY + btnHeight / 2; + + if (isStopped) { + // Draw play icon + var playSize = 10; // Side length of the play triangle + var offset = playSize / Math.sqrt(3) - 3; + g.fillPoly([ + iconX - playSize, iconY - playSize + offset, + iconX - playSize, iconY + playSize + offset, + iconX + playSize * 2 / Math.sqrt(3), iconY + offset + ]); + } else { + // Draw pause icon + var barWidth = 5; // Width of pause bars + var barHeight = btnHeight / 2; // Height of pause bars + var barSpacing = 5; // Spacing between pause bars + g.fillRect(iconX - barSpacing / 2 - barWidth, iconY - barHeight / 2, iconX - barSpacing / 2, iconY + barHeight / 2); + g.fillRect(iconX + barSpacing / 2, iconY - barHeight / 2, iconX + barSpacing / 2 + barWidth, iconY + barHeight / 2); + } + }; + + let drawHamburgerMenu = ()=>{ + var x = g.getWidth() / 2; // Center the hamburger menu horizontally + var y = (7/8)*g.getHeight(); // Position it near the bottom + var lineLength = 18; // Length of the hamburger lines + var spacing = 6; // Space between the lines + + g.setColor(themeColors.fg); // Set color to foreground color for the icon + // Draw three horizontal lines + for (var i = -1; i <= 1; i++) { + g.fillRect(x - lineLength/2, y + i * spacing - 1, x + lineLength/2, y + i * spacing + 1); + } + }; + + // Function to draw the task name centered between the widget field and the start/stop button + let drawTaskName = ()=>{ + g.setFont("Vector", 20); // Set a smaller font for the task name display + + // Calculate position to center the task name horizontally + var x = (g.getWidth()) / 2; + + // Calculate position to center the task name vertically between the widget field and the start/stop button + var y = g.getHeight()/4; // Center vertically + + g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color + g.drawString(currentTask, x, y); // Draw the task name centered on the screen + }; + + // Function to draw the last log entry of the current task + let drawLastLogEntry = ()=>{ + g.setFont("Vector", 10); // Set a smaller font for the task name display + + // Calculate position to center the log entry horizontally + var x = (g.getWidth()) / 2; + + // Calculate position to place the log entry properly between the start/stop button and hamburger menu + var btnBottomY = (g.getHeight() + 50) / 2; // Y-coordinate of the bottom of the start/stop button + var menuBtnYTop = g.getHeight() * (5 / 6); // Y-coordinate of the top of the hamburger menu button + var y = btnBottomY + (menuBtnYTop - btnBottomY) / 2 + 2; // Center vertically between button and menu + + g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color + g.drawString(g.wrapString(tasks[currentTask].lastLine, 150).join("\n"), x, y); + }; + + /* + // Helper function to read the last log entry from the current task's log file + let updateLastLogEntry = ()=>{ + var filename = tasks[currentTask].file; + var file = require("Storage").open(filename, "r"); + var lastLine = ""; + var line; + while ((line = file.readLine()) !== undefined) { + lastLine = line; // Keep reading until the last line + } + tasks[currentTask].lastLine = lastLine; + }; + */ + + // Main UI drawing function + let drawMainMenu = ()=>{ + g.clear(); + Bangle.drawWidgets(); // Draw any active widgets + g.setColor(themeColors.bg); // Set color to theme's background color + g.fillRect(Bangle.appRect); // Fill the app area with the background color + + drawTaskName(); // Draw the centered task name + drawLastLogEntry(); // Draw the last log entry of the current task + drawButton(); // Draw the Start/Stop toggle button + drawHamburgerMenu(); // Draw the hamburger menu button icon + + //g.flip(); // Send graphics to the display + }; + + // Function to toggle the active state + let toggleChronlog = ()=>{ + var dateObj = new Date(); + var dateObjStrSplit = dateObj.toString().split(" "); + var currentTime = dateObj.getFullYear().toString() + "-" + (dateObj.getMonth()<10?"0":"") + dateObj.getMonth().toString() + "-" + (dateObj.getDate()<10?"0":"") + dateObj.getDate().toString() + "T" + (dateObj.getHours()<10?"0":"") + dateObj.getHours().toString() + ":" + (dateObj.getMinutes()<10?"0":"") + dateObj.getMinutes().toString() + ":" + (dateObj.getSeconds()<10?"0":"") + dateObj.getSeconds().toString() + " " + dateObjStrSplit[dateObjStrSplit.length-1]; + + tasks[currentTask].lineNumber = Number(tasks[currentTask].lineNumber) + 1; + logEntry = tasks[currentTask].lineNumber + (tasks[currentTask].state === "stopped" ? ",Start," : ",Stop,") + currentTime + "\n"; + var filename = tasks[currentTask].file; + + // Open the appropriate file and append the log entry + var file = require("Storage").open(filename, "a"); + file.write(logEntry); + tasks[currentTask].lastLine = logEntry; + + // Toggle the state and update the button text + tasks[currentTask].state = tasks[currentTask].state === "stopped" ? "started" : "stopped"; + drawMainMenu(); // Redraw the main UI + }; + + // Define the touch handler function for the main menu + let handleMainMenuTouch = (button, xy)=>{ + var btnTopY = (g.getHeight() - 50) / 2; + var btnBottomY = btnTopY + 50; + var menuBtnYTop = (7/8)*g.getHeight() - 15; + var menuBtnYBottom = (7/8)*g.getHeight() + 15; + var menuBtnXLeft = (g.getWidth() / 2) - 15; + var menuBtnXRight = (g.getWidth() / 2) + 15; + + // Detect if the touch is within the toggle button area + if (xy.x >= 20 && xy.x <= (g.getWidth() - 20) && xy.y > btnTopY && xy.y < btnBottomY) { + toggleChronlog(); + } + // Detect if the touch is within the hamburger menu button area + else if (xy.x >= menuBtnXLeft && xy.x <= menuBtnXRight && xy.y >= menuBtnYTop && xy.y <= menuBtnYBottom) { + showMenu(); + } + }; + + // Function to attach the touch event listener + let setMainUI = ()=>{ + Bangle.setUI({ + mode: "custom", + back: load, + touch: handleMainMenuTouch + }); + }; + + let saveAppState = ()=>{ + let appData = { + currentTask : currentTask, + tasks : tasks, + }; + require("Storage").writeJSON("chronlog.json", appData); + }; + // Set up a listener for the 'kill' event + E.on('kill', saveAppState); + + // Function to switch to a selected task + let switchTask = (taskName)=>{ + currentTask = taskName; // Update the current task + + // Reinitialize the UI elements + setMainUI(); + drawMainMenu(); // Redraw UI to reflect the task change and the button state + }; + + // Function to create a new task + let createNewTask = ()=>{ + // Prompt the user to input the task's name + require("textinput").input({ + text: "" // Default empty text for new task + }).then(result => { + var taskName = result; // Store the result from text input + if (taskName) { + if (tasks.hasOwnProperty(taskName)) { + // Task already exists, handle this case as needed + E.showAlert(/*LANG*/"Task already exists", "Error").then(drawMainMenu); + } else { + // Create a new task log file for the new task + var filename = "chronlog_" + taskName.replace(/\W+/g, "_") + ".csv"; + tasks[taskName] = { + file : filename, + state : "stopped", + lineNumber : 0, + lastLine : "", + lastSyncedLine : "", + }; + + currentTask = taskName; + + setMainUI(); + drawMainMenu(); // Redraw UI with the new task + } + } else { + setMainUI(); + drawMainMenu(); // User cancelled, redraw main menu + } + }).catch(e => { + console.log("Text input error", e); + setMainUI(); + drawMainMenu(); // In case of error also redraw main menu + }); + }; + + // Function to display the list of tasks for selection + let chooseTask = ()=>{ + // Construct the tasks menu from the tasks object + var taskMenu = { + "": { "title": /*LANG*/"Choose Task", + "back" : function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Cancel task selection + } + } + }; + for (var taskName in tasks) { + if (!tasks.hasOwnProperty(taskName)) continue; + taskMenu[taskName] = (function(name) { + return function() { + switchTask(name); + }; + })(taskName); + } + + // Add a menu option for creating a new task + taskMenu[/*LANG*/"Create New Task"] = createNewTask; + + E.showMenu(taskMenu); // Display the task selection + }; + + // Function to annotate the current or last work session + let annotateTask = ()=>{ + + // Prompt the user to input the annotation text + require("textinput").input({ + text: "" // Default empty text for annotation + }).then(result => { + var annotationText = result.trim(); + if (annotationText) { + // Append annotation to the last or current log entry + tasks[currentTask].lineNumber ++; + var annotatedEntry = tasks[currentTask].lineNumber + /*LANG*/",Note," + annotationText + "\n"; + var filename = tasks[currentTask].file; + var file = require("Storage").open(filename, "a"); + file.write(annotatedEntry); + tasks[currentTask].lastLine = annotatedEntry; + setMainUI(); + drawMainMenu(); // Redraw UI after adding the annotation + } else { + // User cancelled, so we do nothing and just redraw the main menu + setMainUI(); + drawMainMenu(); + } + }).catch(e => { + console.log("Annotation input error", e); + setMainUI(); + drawMainMenu(); // In case of error also redraw main menu + }); + }; + + let syncToAndroid = (taskName, isFullSync)=>{ + let mode = "a"; + if (isFullSync) mode = "w"; + let lastSyncedLine = tasks[taskName].lastSyncedLine || 0; + let taskNameValidFileName = taskName.replace(" ","_"); // FIXME: Should use something similar to replaceAll using a regular expression to catch all illegal characters. + + let storageFile = require("Storage").open("chronlog_"+taskNameValidFileName+".csv", "r"); + let contents = storageFile.readLine(); + let lineNumber = contents ? contents.slice(0, contents.indexOf(",")) : 0; + let shouldSyncLine = ()=>{return (contents && (isFullSync || (Number(lineNumber)>Number(lastSyncedLine))));}; + let doSyncLine = (mde)=>{Bluetooth.println(JSON.stringify({t:"file", n:"chronlog_"+taskNameValidFileName+".csv", c:contents, m:mde}));}; + + if (shouldSyncLine()) doSyncLine(mode); + contents = storageFile.readLine(); + while (contents) { + lineNumber = contents.slice(0, contents.indexOf(",")); // Could theoretically do with `lineNumber++`, but this is more robust in case numbering in file ended up irregular. + if (shouldSyncLine()) doSyncLine("a"); + contents = storageFile.readLine(); + } + tasks[taskName].lastSyncedLine = lineNumber; + }; + + // Function to display the list of tasks for selection + let syncTasks = ()=>{ + let isToDoFullSync = false; + // Construct the tasks menu from the tasks object + var syncMenu = { + "": { "title": /*LANG*/"Sync Tasks", + "back" : function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Cancel task selection + } + } + }; + syncMenu[/*LANG*/"Full Resyncs"] = { + value: !!isToDoFullSync, // !! converts undefined to false + onchange: ()=>{ + isToDoFullSync = !isToDoFullSync + }, + } + for (var taskName in tasks) { + if (!tasks.hasOwnProperty(taskName)) continue; + syncMenu[taskName] = (function(name) { + return function() {syncToAndroid(name,isToDoFullSync);}; + })(taskName); + } + + E.showMenu(syncMenu); // Display the task selection + }; + + let showMenu = ()=>{ + var menu = { + "": { "title": /*LANG*/"Menu", + "back": function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Redraw the main UI when closing the menu + }, + }, + /*LANG*/"Annotate": annotateTask, // Now calls the real annotation function + /*LANG*/"Change Task": chooseTask, // Opens the task selection screen + /*LANG*/"Sync to Android": syncTasks, + }; + E.showMenu(menu); + }; + + Bangle.loadWidgets(); + drawMainMenu(); // Draw the main UI when the app starts + // When the application starts, attach the touch event listener + setMainUI(); +} diff --git a/apps/chronlog/app.png b/apps/chronlog/app.png new file mode 100644 index 000000000..c21a147ea Binary files /dev/null and b/apps/chronlog/app.png differ diff --git a/apps/chronlog/dump.png b/apps/chronlog/dump.png new file mode 100644 index 000000000..0c40b190e Binary files /dev/null and b/apps/chronlog/dump.png differ diff --git a/apps/chronlog/dump1.png b/apps/chronlog/dump1.png new file mode 100644 index 000000000..04f625f04 Binary files /dev/null and b/apps/chronlog/dump1.png differ diff --git a/apps/chronlog/dump2.png b/apps/chronlog/dump2.png new file mode 100644 index 000000000..be0791659 Binary files /dev/null and b/apps/chronlog/dump2.png differ diff --git a/apps/chronlog/dump3.png b/apps/chronlog/dump3.png new file mode 100644 index 000000000..eeeba525f Binary files /dev/null and b/apps/chronlog/dump3.png differ diff --git a/apps/chronlog/dump4.png b/apps/chronlog/dump4.png new file mode 100644 index 000000000..b1bd51669 Binary files /dev/null and b/apps/chronlog/dump4.png differ diff --git a/apps/chronlog/dump5.png b/apps/chronlog/dump5.png new file mode 100644 index 000000000..debf919cc Binary files /dev/null and b/apps/chronlog/dump5.png differ diff --git a/apps/chronlog/dump6.png b/apps/chronlog/dump6.png new file mode 100644 index 000000000..29a06b68f Binary files /dev/null and b/apps/chronlog/dump6.png differ diff --git a/apps/chronlog/metadata.json b/apps/chronlog/metadata.json new file mode 100644 index 000000000..8ed618b27 --- /dev/null +++ b/apps/chronlog/metadata.json @@ -0,0 +1,14 @@ +{ "id": "chronlog", + "name": "Chrono Logger", + "version":"0.01", + "description": "Record time active on a task, course, work or anything really.", + "icon": "app.png", + "tags": "logging, record, work, tasks", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots" : [ { "url":"dump.png"}, { "url":"dump1.png" }, { "url":"dump2.png" }, { "url":"dump3.png" }, { "url":"dump4.png" }, { "url":"dump5.png" }, { "url":"dump6.png" } ], + "storage": [ + {"name":"chronlog.app.js","url":"app.js"}, + {"name":"chronlog.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/clkinfogpsspeed/ChangeLog b/apps/clkinfogpsspeed/ChangeLog new file mode 100644 index 000000000..78ba28f3b --- /dev/null +++ b/apps/clkinfogpsspeed/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock Info! diff --git a/apps/clkinfogpsspeed/clkinfo.js b/apps/clkinfogpsspeed/clkinfo.js new file mode 100644 index 000000000..a2c1f51c1 --- /dev/null +++ b/apps/clkinfogpsspeed/clkinfo.js @@ -0,0 +1,27 @@ +(function() { + var speed; + function gpsHandler(e) { + speed = e.speed; + ci.items[0].emit('redraw'); + } + var ci = { + name: "GPS", + items: [ + { name : "Speed", + get : function() { return { text : isFinite(speed) ? require("locale").speed(speed) : "--", + v : 0, min : isFinite(speed) ? speed : 0, max : 150, + img : atob("GBiBAAAAAAAAAAAAAAAAAAD/AAHDgAMYwAbDYAwAMAoA0BgDmBgfGB4ceBgYGBgAGBoAWAwAMAwAMAf/4AP/wAAAAAAAAAAAAAAAAA==") }}, + show : function() { + Bangle.setGPSPower(1, "clkinfogpsspeed"); + Bangle.on("GPS", gpsHandler); + }, + hide : function() { + Bangle.removeListener("GPS", gpsHandler); + Bangle.setGPSPower(0, "clkinfogpsspeed"); + } + // run : function() {} optional (called when tapped) + } + ] + }; + return ci; +}) // must not have a semi-colon! \ No newline at end of file diff --git a/apps/clkinfogpsspeed/icon.png b/apps/clkinfogpsspeed/icon.png new file mode 100644 index 000000000..10186f13f Binary files /dev/null and b/apps/clkinfogpsspeed/icon.png differ diff --git a/apps/clkinfogpsspeed/metadata.json b/apps/clkinfogpsspeed/metadata.json new file mode 100644 index 000000000..7fceeb7b8 --- /dev/null +++ b/apps/clkinfogpsspeed/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfogpsspeed", + "name": "GPS Speed Clockinfo", + "shortName":"GPS Speed", + "version":"0.01", + "description": "A Clockinfo that displays your current speed according to the GPS", + "icon": "icon.png", + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfogpsspeed.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfosec/ChangeLog b/apps/clkinfosec/ChangeLog new file mode 100644 index 000000000..11fefd24b --- /dev/null +++ b/apps/clkinfosec/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Remove 's' after seconds (on some clocks this looks like '5') \ No newline at end of file diff --git a/apps/clkinfosec/app.png b/apps/clkinfosec/app.png new file mode 100644 index 000000000..ed79cd884 Binary files /dev/null and b/apps/clkinfosec/app.png differ diff --git a/apps/clkinfosec/clkinfo.js b/apps/clkinfosec/clkinfo.js new file mode 100644 index 000000000..d407228db --- /dev/null +++ b/apps/clkinfosec/clkinfo.js @@ -0,0 +1,33 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "Seconds", + get : () => { + let d = new Date(), s = d.getSeconds(), sr = s*Math.PI/30, + x = 11+9*Math.sin(sr), y = 11-9*Math.cos(sr), + g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.transparent = 0; + g.drawImage(atob("GBgBAP4AA/+ABwHAHABwGAAwMAAYYAAMYAAMwAAGwAAGwAAGwAAGwAAGwAAGwAAGYAAMYAAMMAAYGAAwHABwBwHAA/+AAP4AAAAA")); + g.drawLine(11,11,x,y).drawLine(12,11,x+1,y).drawLine(11,12,x,y+1).drawLine(12,12,x+1,y+1); + return { + text : s.toString().padStart(2,0), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 1000); + }, 1000 - (Date.now() % 1000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + } + ] + }; +}) \ No newline at end of file diff --git a/apps/clkinfosec/metadata.json b/apps/clkinfosec/metadata.json new file mode 100644 index 000000000..aaf96e9bb --- /dev/null +++ b/apps/clkinfosec/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfosec", + "name": "Seconds Clockinfo", + "version":"0.02", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the time in seconds (many clocks only display minutes)", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo,seconds,time", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfosec.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfosec/screenshot.png b/apps/clkinfosec/screenshot.png new file mode 100644 index 000000000..bb054e3a4 Binary files /dev/null and b/apps/clkinfosec/screenshot.png differ diff --git a/apps/clockbg/ChangeLog b/apps/clockbg/ChangeLog new file mode 100644 index 000000000..026dc1aa0 --- /dev/null +++ b/apps/clockbg/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Moved settings into 'Settings->Apps' +0.03: Add 'Squares' option for random squares background \ No newline at end of file diff --git a/apps/clockbg/README.md b/apps/clockbg/README.md new file mode 100644 index 000000000..14bbeb7a7 --- /dev/null +++ b/apps/clockbg/README.md @@ -0,0 +1,46 @@ +# Clock Backgrounds + +This app provides a library (`clockbg`) that can be used by clocks to +provide different backgrounds for them. + +## Usage + +By default the app provides just a red/green/blue background but it can easily be configured. + +You can either: + +* Go to [the Clock Backgrounds app](https://banglejs.com/apps/?id=clockbg) in the App Loader and use pre-made image backgrounds (or upload your own) +* Go to the `Backgrounds` app on the Bangle itself, and choose between: + * `Solid Color` - one color that never changes + * `Random Color` - a new color every time the clock starts + * `Image` - choose from a previously uploaded image + * `Squares` - a randomly generated pattern of squares in the selected color palette + + +## Usage in code + +Just use the following to use this library within your code: + +```JS +// once at the start +let background = require("clockbg"); + +// to fill the whole area +background.fillRect(Bangle.appRect); + +// to fill just one part of the screen +background.fillRect(x1, y1, x2, y2); +``` + +You should also add `"dependencies" : { "clockbg":"module" },` to your app's metadata to +ensure that the clock background library is automatically loaded. + +## Features to be added + +A few features could be added that would really improve functionality: + +* When 'fast loading', 'random' backgrounds don't update at the moment +* Support for >1 image to be uploaded (requires some image management in `interface.html`), and choose randomly between them +* Support for gradients (random colors) +* More types of auto-generated pattern (as long as they can be generated quickly or in the background) +* Storing 'clear' areas of uploaded images so clocks can easily position themselves \ No newline at end of file diff --git a/apps/clockbg/app.png b/apps/clockbg/app.png new file mode 100644 index 000000000..21e39a4e5 Binary files /dev/null and b/apps/clockbg/app.png differ diff --git a/apps/clockbg/img/README.md b/apps/clockbg/img/README.md new file mode 100644 index 000000000..17846e6b8 --- /dev/null +++ b/apps/clockbg/img/README.md @@ -0,0 +1,19 @@ +Clock Images +============= + +If you want to add your own images ensure they're in the same style and then also list the image file in custom.html in the root directory. + +## Flags + +The flags come from https://icons8.com/icon/set/flags/color and are 480x480px + +If your flag is listed in https://icons8.com/icon/set/flags/color and you can't download it in the right size, please file an issue and we'll download it with our account. + + +## Other backgrounds + +Backgrounds prefixed `ai_` are generated by the AI [Bing Image Creator](https://www.bing.com/images/create) + + + + diff --git a/apps/clockbg/img/ai_eye.jpeg b/apps/clockbg/img/ai_eye.jpeg new file mode 100644 index 000000000..e41d40457 Binary files /dev/null and b/apps/clockbg/img/ai_eye.jpeg differ diff --git a/apps/clockbg/img/ai_flow.jpeg b/apps/clockbg/img/ai_flow.jpeg new file mode 100644 index 000000000..f30f89843 Binary files /dev/null and b/apps/clockbg/img/ai_flow.jpeg differ diff --git a/apps/clockbg/img/ai_hero.jpeg b/apps/clockbg/img/ai_hero.jpeg new file mode 100644 index 000000000..e0bad0895 Binary files /dev/null and b/apps/clockbg/img/ai_hero.jpeg differ diff --git a/apps/clockbg/img/ai_horse.jpeg b/apps/clockbg/img/ai_horse.jpeg new file mode 100644 index 000000000..3b7da61f1 Binary files /dev/null and b/apps/clockbg/img/ai_horse.jpeg differ diff --git a/apps/clockbg/img/ai_robot.jpeg b/apps/clockbg/img/ai_robot.jpeg new file mode 100644 index 000000000..a493a5c6a Binary files /dev/null and b/apps/clockbg/img/ai_robot.jpeg differ diff --git a/apps/clockbg/img/ai_rock.jpeg b/apps/clockbg/img/ai_rock.jpeg new file mode 100644 index 000000000..18cbe63b2 Binary files /dev/null and b/apps/clockbg/img/ai_rock.jpeg differ diff --git a/apps/clockbg/img/icons8-australia-480.png b/apps/clockbg/img/icons8-australia-480.png new file mode 100644 index 000000000..3bb1ac09b Binary files /dev/null and b/apps/clockbg/img/icons8-australia-480.png differ diff --git a/apps/clockbg/img/icons8-austria-480.png b/apps/clockbg/img/icons8-austria-480.png new file mode 100644 index 000000000..5431bbd7a Binary files /dev/null and b/apps/clockbg/img/icons8-austria-480.png differ diff --git a/apps/clockbg/img/icons8-belgium-480.png b/apps/clockbg/img/icons8-belgium-480.png new file mode 100644 index 000000000..f0c6485f9 Binary files /dev/null and b/apps/clockbg/img/icons8-belgium-480.png differ diff --git a/apps/clockbg/img/icons8-brazil-480.png b/apps/clockbg/img/icons8-brazil-480.png new file mode 100644 index 000000000..505af26ca Binary files /dev/null and b/apps/clockbg/img/icons8-brazil-480.png differ diff --git a/apps/clockbg/img/icons8-canada-480.png b/apps/clockbg/img/icons8-canada-480.png new file mode 100644 index 000000000..62181468f Binary files /dev/null and b/apps/clockbg/img/icons8-canada-480.png differ diff --git a/apps/clockbg/img/icons8-china-480.png b/apps/clockbg/img/icons8-china-480.png new file mode 100644 index 000000000..ba56ccc1e Binary files /dev/null and b/apps/clockbg/img/icons8-china-480.png differ diff --git a/apps/clockbg/img/icons8-denmark-480.png b/apps/clockbg/img/icons8-denmark-480.png new file mode 100644 index 000000000..716e15f98 Binary files /dev/null and b/apps/clockbg/img/icons8-denmark-480.png differ diff --git a/apps/clockbg/img/icons8-england-480.png b/apps/clockbg/img/icons8-england-480.png new file mode 100644 index 000000000..2343521f3 Binary files /dev/null and b/apps/clockbg/img/icons8-england-480.png differ diff --git a/apps/clockbg/img/icons8-flag-of-europe-480.png b/apps/clockbg/img/icons8-flag-of-europe-480.png new file mode 100644 index 000000000..616d5fe7a Binary files /dev/null and b/apps/clockbg/img/icons8-flag-of-europe-480.png differ diff --git a/apps/clockbg/img/icons8-france-480.png b/apps/clockbg/img/icons8-france-480.png new file mode 100644 index 000000000..09a24c8b1 Binary files /dev/null and b/apps/clockbg/img/icons8-france-480.png differ diff --git a/apps/clockbg/img/icons8-germany-480.png b/apps/clockbg/img/icons8-germany-480.png new file mode 100644 index 000000000..dd2b317bb Binary files /dev/null and b/apps/clockbg/img/icons8-germany-480.png differ diff --git a/apps/clockbg/img/icons8-great-britain-480.png b/apps/clockbg/img/icons8-great-britain-480.png new file mode 100644 index 000000000..f89add414 Binary files /dev/null and b/apps/clockbg/img/icons8-great-britain-480.png differ diff --git a/apps/clockbg/img/icons8-greece-480.png b/apps/clockbg/img/icons8-greece-480.png new file mode 100644 index 000000000..eb823b4bc Binary files /dev/null and b/apps/clockbg/img/icons8-greece-480.png differ diff --git a/apps/clockbg/img/icons8-hungary-480.png b/apps/clockbg/img/icons8-hungary-480.png new file mode 100644 index 000000000..4097e88d3 Binary files /dev/null and b/apps/clockbg/img/icons8-hungary-480.png differ diff --git a/apps/clockbg/img/icons8-italy-480.png b/apps/clockbg/img/icons8-italy-480.png new file mode 100644 index 000000000..82b1b710e Binary files /dev/null and b/apps/clockbg/img/icons8-italy-480.png differ diff --git a/apps/clockbg/img/icons8-lgbt-flag-480.png b/apps/clockbg/img/icons8-lgbt-flag-480.png new file mode 100644 index 000000000..2e8bbebb1 Binary files /dev/null and b/apps/clockbg/img/icons8-lgbt-flag-480.png differ diff --git a/apps/clockbg/img/icons8-netherlands-480.png b/apps/clockbg/img/icons8-netherlands-480.png new file mode 100644 index 000000000..4ea397e27 Binary files /dev/null and b/apps/clockbg/img/icons8-netherlands-480.png differ diff --git a/apps/clockbg/img/icons8-new-zealand-480.png b/apps/clockbg/img/icons8-new-zealand-480.png new file mode 100644 index 000000000..e21fdc574 Binary files /dev/null and b/apps/clockbg/img/icons8-new-zealand-480.png differ diff --git a/apps/clockbg/img/icons8-norway-480.png b/apps/clockbg/img/icons8-norway-480.png new file mode 100644 index 000000000..a57c0f7fb Binary files /dev/null and b/apps/clockbg/img/icons8-norway-480.png differ diff --git a/apps/clockbg/img/icons8-scotland-480.png b/apps/clockbg/img/icons8-scotland-480.png new file mode 100644 index 000000000..20f08cfbb Binary files /dev/null and b/apps/clockbg/img/icons8-scotland-480.png differ diff --git a/apps/clockbg/img/icons8-spain-480.png b/apps/clockbg/img/icons8-spain-480.png new file mode 100644 index 000000000..17fed3360 Binary files /dev/null and b/apps/clockbg/img/icons8-spain-480.png differ diff --git a/apps/clockbg/img/icons8-sweden-480.png b/apps/clockbg/img/icons8-sweden-480.png new file mode 100644 index 000000000..99299e93a Binary files /dev/null and b/apps/clockbg/img/icons8-sweden-480.png differ diff --git a/apps/clockbg/img/icons8-switzerland-480.png b/apps/clockbg/img/icons8-switzerland-480.png new file mode 100644 index 000000000..cde8c6ff0 Binary files /dev/null and b/apps/clockbg/img/icons8-switzerland-480.png differ diff --git a/apps/clockbg/img/icons8-ukraine-480.png b/apps/clockbg/img/icons8-ukraine-480.png new file mode 100644 index 000000000..718695b38 Binary files /dev/null and b/apps/clockbg/img/icons8-ukraine-480.png differ diff --git a/apps/clockbg/img/icons8-usa-480.png b/apps/clockbg/img/icons8-usa-480.png new file mode 100644 index 000000000..a7867f070 Binary files /dev/null and b/apps/clockbg/img/icons8-usa-480.png differ diff --git a/apps/clockbg/img/icons8-wales-480.png b/apps/clockbg/img/icons8-wales-480.png new file mode 100644 index 000000000..58233db34 Binary files /dev/null and b/apps/clockbg/img/icons8-wales-480.png differ diff --git a/apps/clockbg/interface.html b/apps/clockbg/interface.html new file mode 100644 index 000000000..c92600b1b --- /dev/null +++ b/apps/clockbg/interface.html @@ -0,0 +1,202 @@ + + + + + + +

Upload an image:

+
+

If you'd like to contribute images you can add them on GitHub!

+
Preview:
+
+
+ +
+ +
+ + +

Click

+ + + + + + + + \ No newline at end of file diff --git a/apps/clockbg/lib.js b/apps/clockbg/lib.js new file mode 100644 index 000000000..c9b1fb1d2 --- /dev/null +++ b/apps/clockbg/lib.js @@ -0,0 +1,37 @@ +let settings = Object.assign({ + style : "randomcolor", + colors : ["#F00","#0F0","#00F"] +},require("Storage").readJSON("clockbg.json")||{}); +if (settings.style=="image") + settings.img = require("Storage").read(settings.fn); +else if (settings.style=="randomcolor") { + settings.style = "color"; + let n = (0|(Math.random()*settings.colors.length)) % settings.colors.length; + settings.color = settings.colors[n]; + delete settings.colors; +} else if (settings.style=="squares") { + settings.style = "image"; + let bpp = (settings.colors.length>4)?4:2; + let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true}); + E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256); // random pixels + bg.palette = new Uint16Array(1<g.toColor(c))); + settings.img = bg.asImage("string"); + settings.imgOpt = {scale:16}; + delete settings.colors; +} + +// Fill a rectangle with the current background style, rect = {x,y,w,h} +// eg require("clockbg").fillRect({x:10,y:10,w:50,h:50}) +// require("clockbg").fillRect(Bangle.appRect) +exports.fillRect = function(rect,y,x2,y2) { + if ("object"!=typeof rect) rect = {x:rect,y:y,w:1+x2-rect,h:1+y2-y}; + if (settings.img) { + g.setClipRect(rect.x, rect.y, rect.x+rect.w-1, rect.y+rect.h-1).drawImage(settings.img,0,0,settings.imgOpt).setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + } else if (settings.style == "color") { + g.setBgColor(settings.color).clearRect(rect); + } else { + console.log("clockbg: No background set!"); + g.setBgColor(g.theme.bg).clearRect(rect); + } +}; \ No newline at end of file diff --git a/apps/clockbg/metadata.json b/apps/clockbg/metadata.json new file mode 100644 index 000000000..2221e99bd --- /dev/null +++ b/apps/clockbg/metadata.json @@ -0,0 +1,21 @@ +{ "id": "clockbg", + "name": "Clock Backgrounds", + "shortName":"Backgrounds", + "version": "0.03", + "description": "Library that allows clocks to include a custom background (generated on demand or uploaded).", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], + "type": "module", + "readme": "README.md", + "provides_modules" : ["clockbg"], + "tags": "module,background", + "supports" : ["BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name":"clockbg","url":"lib.js"}, + {"name":"clockbg.settings.js","url":"settings.js"} + ], "data": [ + {"wildcard":"clockbg.bg*.img"}, + {"name":"clockbg.json"} + ] +} diff --git a/apps/clockbg/screenshot.png b/apps/clockbg/screenshot.png new file mode 100644 index 000000000..f9d395e74 Binary files /dev/null and b/apps/clockbg/screenshot.png differ diff --git a/apps/clockbg/screenshot2.png b/apps/clockbg/screenshot2.png new file mode 100644 index 000000000..819e8ca87 Binary files /dev/null and b/apps/clockbg/screenshot2.png differ diff --git a/apps/clockbg/settings.js b/apps/clockbg/settings.js new file mode 100644 index 000000000..c39017262 --- /dev/null +++ b/apps/clockbg/settings.js @@ -0,0 +1,122 @@ +(function(back) { +let settings = Object.assign({ + style : "randomcolor", + colors : ["#F00","#0F0","#00F"] +},require("Storage").readJSON("clockbg.json")||{}); + +function saveSettings() { + if (settings.style!="image") + delete settings.fn; + if (settings.style!="color") + delete settings.color; + if (settings.style!="randomcolor" && settings.style!="squares") + delete settings.colors; + require("Storage").writeJSON("clockbg.json", settings); +} + +function getColorsImage(cols) { + var bpp = 1; + if (cols.length>4) bpp=4; + else if (cols.length>2) bpp=2; + var w = (cols.length>8)?8:16; + var b = Graphics.createArrayBuffer(w*cols.length,16,bpp); + b.palette = new Uint16Array(1<{ + b.setColor(i).fillRect(i*w,0,i*w+w-1,15); + b.palette[i] = g.toColor(c); + }); + return "\0"+b.asImage("string"); +} + +function showModeMenu() { + E.showMenu({ + "" : {title:/*LANG*/"Background", back:showMainMenu}, + /*LANG*/"Solid Color" : function() { + var cols = ["#F00","#0F0","#FF0", + "#00F","#F0F","#0FF", + "#000","#888","#fff",]; + var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}}; + cols.forEach(col => { + menu["-"+getColorsImage([col])] = () => { + settings.style = "color"; + settings.color = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + }, + /*LANG*/"Random Color" : function() { + var cols = [ + ["#F00","#0F0","#FF0","#00F","#F0F","#0FF"], + ["#F00","#0F0","#00F"], + // Please add some more! + ]; + var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "randomcolor"; + settings.colors = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + }, + /*LANG*/"Image" : function() { + let images = require("Storage").list(/clockbg\..*\.img/); + if (images.length) { + var menu = {"":{title:/*LANG*/"Images", back:showModeMenu}}; + images.forEach(im => { + menu[im.slice(8,-4)] = () => { + settings.style = "image"; + settings.fn = im; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + } else { + E.showAlert("Please use App Loader to upload images").then(showModeMenu); + } + }, + /*LANG*/"Squares" : function() { + /* + a = new Array(16); + a.fill(0); + print(a.map((n,i)=>E.HSBtoRGB(0 + i/16,1,1,24).toString(16).padStart(6,0).replace(/(.).(.).(.)./,"\"#$1$2$3\"")).join(",")) + */ + var cols = [ // list of color palettes used as possible square colours - either 4 or 16 entries + ["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"], + ["#0FF","#0CC","#088","#044"], + ["#FFF","#FBB","#F66","#F44"], + ["#FFF","#BBB","#666","#000"] + // Please add some more! + ]; + var menu = {"":{title:/*LANG*/"Squares", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "squares"; + settings.colors = col; + console.log(settings); + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + } + }); +} + +function showMainMenu() { + E.showMenu({ + "" : {title:/*LANG*/"Clock Background", back:back}, + /*LANG*/"Mode" : { + value : settings.style, + onchange : showModeMenu + } + }); +} + +showMainMenu(); +}) \ No newline at end of file diff --git a/apps/counter2/ChangeLog b/apps/counter2/ChangeLog new file mode 100644 index 000000000..58eacf613 --- /dev/null +++ b/apps/counter2/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Added Settings & readme +0.03: Fix lint warnings +0.04: Fix lint warnings diff --git a/apps/counter2/README.md b/apps/counter2/README.md new file mode 100644 index 000000000..d57844aae --- /dev/null +++ b/apps/counter2/README.md @@ -0,0 +1,24 @@ +# Counter2 by Michael + +I needed an HP/XP-Tracker for a game, so i made one. +The counter state gets saved. Best to use this with pattern launcher or ClockCal + +- Colored Background Mode +- ![color bg](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2-screenshot.png) +- Colored Text Mode +- ![color text](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2dark-screenshot.png) + +## Howto + - Tap top side or swipe up to increase counter + - Tap bottom side or swipe down to decrease counter + - Hold (600ms) to reset to default value (configurable) + - Press button to exit + +## Configurable Features +- Default value Counter 1 +- Default value Counter 2 +- Buzz on interact +- Colored Text/Background + +## Feedback +If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues diff --git a/apps/counter2/app-icon.js b/apps/counter2/app-icon.js new file mode 100644 index 000000000..fda8d1e21 --- /dev/null +++ b/apps/counter2/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcAyVJkgCFAwwCBAgd5CI+eCI2T/IRH/wR7n//AAPyCIdPBAX8CKpr/CLTpSCOipB8gRFXoPJCIknCJAIBOoYRCagLNCa4f8Q4gREI4tP8mT/41HCKJHFGoQRG+QKBLI4RHLIx9CCJ7zBGpxZCPoyhQYpIIBYor7kCP4R8YoX/WY69DAIM/BAT+BdIYICeYQRTGqKP/CNIA==")) \ No newline at end of file diff --git a/apps/counter2/app.js b/apps/counter2/app.js new file mode 100644 index 000000000..42b59cf5d --- /dev/null +++ b/apps/counter2/app.js @@ -0,0 +1,95 @@ +Bangle.loadWidgets(); + +var s = Object.assign({ + counter0:10, + counter1:20, + max0:15, + max1:25, + buzz: true, + colortext: true, +}, require('Storage').readJSON("counter2.json", true) || {}); + +const f1 = (s.colortext) ? "#f00" : "#fff"; +const f2 = (s.colortext) ? "#00f" : "#fff"; +const b1 = (s.colortext) ? g.theme.bg : "#f00"; +const b2 = (s.colortext) ? g.theme.bg : "#00f"; + +var drag; + +const screenwidth = g.getWidth(); +const screenheight = g.getHeight(); +const halfwidth = screenwidth / 2; +const halfheight = screenheight / 2; + +const counter = []; +counter[0] = s.counter0; +counter[1] = s.counter1; +const defaults = []; +defaults[0] = s.max0; +defaults[1] = s.max1; + +function saveSettings() { + s.counter0 = counter[0]; + s.counter1 = counter[1]; + s.max0 = defaults[0]; + s.max1 = defaults[1]; + require('Storage').writeJSON("counter2.json", s); +} + +let ignoreonce = false; +var dragtimeout; + +function updateScreen() { + g.setBgColor(b1); + g.clearRect(0, 0, halfwidth, screenheight); + g.setBgColor(b2); + g.clearRect(halfwidth, 0, screenwidth, screenheight); + g.setFont("Vector", 60).setFontAlign(0, 0); + g.setColor(f1); + g.drawString(Math.floor(counter[0]), halfwidth * 0.5, halfheight); + g.setColor(f2); + g.drawString(Math.floor(counter[1]), halfwidth * 1.5, halfheight); + saveSettings(); + if (s.buzz) Bangle.buzz(50,.5); + Bangle.drawWidgets(); +} + +Bangle.on("drag", e => { + const c = (e.x < halfwidth) ? 0 : 1; + if (!drag) { + if (ignoreonce) { + ignoreonce = false; + return; + } + drag = { x: e.x, y: e.y }; + dragtimeout = setTimeout(function () { resetcounter(c); }, 600); //if dragging for 500ms, reset counter + } + else if (drag && !e.b) { // released + let adjust = 0; + const dx = e.x - drag.x, dy = e.y - drag.y; + if (Math.abs(dy) > Math.abs(dx) + 30) { + adjust = (dy > 0) ? -1 : 1; + } else { + adjust = (e.y > halfwidth) ? -1 : 1; + } + counter[c] += adjust; + updateScreen(); + drag = undefined; + clearTimeout(dragtimeout); + } +}); + +function resetcounter(which) { + counter[which] = defaults[which]; + console.log("resetting counter ", which); + updateScreen(); + drag = undefined; + ignoreonce = true; +} + + +updateScreen(); + +setWatch(function() { + load(); +}, BTN1, {repeat:true, edge:"falling"}); diff --git a/apps/counter2/counter2-icon.png b/apps/counter2/counter2-icon.png new file mode 100644 index 000000000..c16e9c0c7 Binary files /dev/null and b/apps/counter2/counter2-icon.png differ diff --git a/apps/counter2/counter2-screenshot.png b/apps/counter2/counter2-screenshot.png new file mode 100644 index 000000000..0864acb64 Binary files /dev/null and b/apps/counter2/counter2-screenshot.png differ diff --git a/apps/counter2/counter2dark-screenshot.png b/apps/counter2/counter2dark-screenshot.png new file mode 100644 index 000000000..2f0fd07c1 Binary files /dev/null and b/apps/counter2/counter2dark-screenshot.png differ diff --git a/apps/counter2/metadata.json b/apps/counter2/metadata.json new file mode 100644 index 000000000..400abf267 --- /dev/null +++ b/apps/counter2/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "counter2", + "name": "Counter2", + "version": "0.04", + "description": "Dual Counter", + "readme":"README.md", + "icon": "counter2-icon.png", + "tags": "tool", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"counter2-screenshot.png"},{"url":"counter2dark-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"counter2.app.js","url":"app.js"}, + {"name":"counter2.settings.js","url":"settings.js"}, + {"name":"counter2.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"counter2.json"}] +} diff --git a/apps/counter2/settings.js b/apps/counter2/settings.js new file mode 100644 index 000000000..b38df1824 --- /dev/null +++ b/apps/counter2/settings.js @@ -0,0 +1,55 @@ +(function (back) { + var FILE = "counter2.json"; + const defaults={ + counter0:12, + counter1:0, + max0:12, + max1:0, + buzz: true, + colortext: true, + }; + const settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + const menu = { + "": { "title": "Counter2" }, + "< Back": () => back(), + 'Default C1': { + value: settings[0], + min: -99, max: 99, + onchange: v => { + settings.max0 = v; + writeSettings(); + } + }, + 'Default C2': { + value: settings[2], + min: -99, max: 99, + onchange: v => { + settings.max1 = v; + writeSettings(); + } + }, + 'Color': { + value: settings.colortext, + format: v => v?"Text":"Backg", + onchange: v => { + settings.colortext = v; + console.log("Color",v); + writeSettings(); + } + }, + 'Vibrate': { + value: settings.buzz, + onchange: v => { + settings.buzz = v; + writeSettings(); + } + } + }; + // Show the menu + E.showMenu(menu); +}); diff --git a/apps/ctrlpad/ChangeLog b/apps/ctrlpad/ChangeLog new file mode 100644 index 000000000..d8c477701 --- /dev/null +++ b/apps/ctrlpad/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app - forked from widhid +0.02: Minor code improvements diff --git a/apps/ctrlpad/README.md b/apps/ctrlpad/README.md new file mode 100644 index 000000000..492957fe7 --- /dev/null +++ b/apps/ctrlpad/README.md @@ -0,0 +1,24 @@ +# Description + +A control pad app to provide fast access to common functions, such as bluetooth power, HRM and Do Not Disturb. +By dragging from the top of the watch, you have this control without leaving your current app (e.g. on a run, bike ride or just watching the clock). + +The app is designed to not conflict with other gestures - when the control pad is visible, it'll prevent propagation of events past it (touch, drag and swipe specifically). When the control pad is hidden, it'll ignore touch, drag and swipe events with the exception of an event dragging from the top 40 pixels of the screen. + + +# Usage + +Swipe down to enable and observe the overlay being dragged in. Swipe up on the overlay to hide it again. Then tap on a given button to trigger it. + +Requires espruino firmware > 2v17 to avoid event handler clashes. + + +# Setup / Technical details + +The control pad disables drag and touch event handlers while active, preventing other apps from interfering. + + +# Todo + +- Handle rotated screen (`g.setRotation(...)`) +- Handle notifications (sharing of `setLCDOverlay`) diff --git a/apps/ctrlpad/icon.js b/apps/ctrlpad/icon.js new file mode 100644 index 000000000..1e139312b --- /dev/null +++ b/apps/ctrlpad/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lcshAzwp9WlgAXldWp8rp5bIq1drwAdq0rFI1XBodXAC4rErorEFIlWLAOCAC2IxGCFY9WA4VWCAQAbJgavBlanCqwodFYpWBp4pCKbwACQYVQfoJUUruBD4dXBQeBBQZWCQIIqBq9dFSNXD4eBFQldDwgqBq4qDP4xEBqwKHFS6qFwVWQ4OsAgYqhAoOtAAYsBFUAbBFImI1uBDIgqQq4qJqwpEwIGCKwgqEroKEFQhsBFRNPwIACVIIECp4qHq16CAKATCAIACqwFEFQxIB6/XRoZVQABwqHLgQqiQAWAQBAqeD4IEDVaLRBABAqJq4qJq5VdwIqKQDwqWQBtXqoUDFQmBCAI2DKq+BvXX6wxCFQb6B6/XEAYqXrurD4N6CoIqDwOBBQIDBQCY1FJQOs1hVIBQgqLwQAFKwwgBVZAKFQDAlCCYYqEBQoqaq4qJrtdFTzJCFX4qoS4gqmCwYqewQqFQIIqhq9XEoNPp4qCQKOBCQeCPQgKEKAdWlYEBrpWSABtWKgNelcAQIdXFbxQBEYQqBgErrpXDq+CADBIBKYRUCAAKCBFYQsCADAoDrzTBFQRWBlZfCADp9BFIgACp4tCq4AYqxMCFAwAEBhgAWA==")) diff --git a/apps/ctrlpad/icon.js.png b/apps/ctrlpad/icon.js.png new file mode 100644 index 000000000..295b31f81 Binary files /dev/null and b/apps/ctrlpad/icon.js.png differ diff --git a/apps/ctrlpad/icon.png b/apps/ctrlpad/icon.png new file mode 100644 index 000000000..39634ea4d Binary files /dev/null and b/apps/ctrlpad/icon.png differ diff --git a/apps/ctrlpad/main.js b/apps/ctrlpad/main.js new file mode 100644 index 000000000..93f2864f7 --- /dev/null +++ b/apps/ctrlpad/main.js @@ -0,0 +1,314 @@ +(function () { + if (!Bangle.prependListener) { + Bangle.prependListener = function (evt, listener) { + var handlers = Bangle["#on".concat(evt)]; + if (!handlers) { + Bangle.on(evt, listener); + } + else { + if (typeof handlers === "function") { + Bangle.on(evt, listener); + } + Bangle["#on".concat(evt)] = [listener].concat(handlers.filter(function (f) { return f !== listener; })); + } + }; + } + var Overlay = (function () { + function Overlay() { + this.width = g.getWidth() - 10 * 2; + this.height = g.getHeight() - 24 - 10; + this.g2 = Graphics.createArrayBuffer(this.width, this.height, 4, { msb: true }); + this.renderG2(); + } + Overlay.prototype.setBottom = function (bottom) { + var g2 = this.g2; + var y = bottom - this.height; + Bangle.setLCDOverlay(g2, 10, y - 10); + }; + Overlay.prototype.hide = function () { + Bangle.setLCDOverlay(); + }; + Overlay.prototype.renderG2 = function () { + this.g2 + .reset() + .setColor(g.theme.bg) + .fillRect(0, 0, this.width, this.height) + .setColor(colour.on.bg) + .drawRect(0, 0, this.width - 1, this.height - 1) + .drawRect(1, 1, this.width - 2, this.height - 2); + }; + return Overlay; + }()); + var colour = { + on: { + fg: "#fff", + bg: "#00a", + }, + off: { + fg: "#000", + bg: "#bbb", + }, + }; + var Controls = (function () { + function Controls(g, controls) { + var height = g.getHeight(); + var centreY = height / 2; + var circleGapY = 30; + var width = g.getWidth(); + this.controls = [ + { x: width / 4 - 10, y: centreY - circleGapY }, + { x: width / 2, y: centreY - circleGapY }, + { x: width * 3 / 4 + 10, y: centreY - circleGapY }, + { x: width / 3, y: centreY + circleGapY }, + { x: width * 2 / 3, y: centreY + circleGapY }, + ].map(function (xy, i) { + var ctrl = xy; + var from = controls[i]; + ctrl.text = from.text; + ctrl.cb = from.cb; + Object.assign(ctrl, from.cb(false) ? colour.on : colour.off); + return ctrl; + }); + } + Controls.prototype.draw = function (g, single) { + g + .setFontAlign(0, 0) + .setFont("4x6:3"); + for (var _i = 0, _a = single ? [single] : this.controls; _i < _a.length; _i++) { + var ctrl = _a[_i]; + g + .setColor(ctrl.bg) + .fillCircle(ctrl.x, ctrl.y, 23) + .setColor(ctrl.fg) + .drawString(ctrl.text, ctrl.x, ctrl.y); + } + }; + Controls.prototype.hitTest = function (x, y) { + var dist = Infinity; + var closest; + for (var _i = 0, _a = this.controls; _i < _a.length; _i++) { + var ctrl = _a[_i]; + var dx = x - ctrl.x; + var dy = y - ctrl.y; + var d = Math.sqrt(dx * dx + dy * dy); + if (d < dist) { + dist = d; + closest = ctrl; + } + } + return dist < 30 ? closest : undefined; + }; + return Controls; + }()); + var state = 0; + var startY = 0; + var startedUpDrag = false; + var upDragAnim; + var ui; + var touchDown = false; + var initUI = function () { + if (ui) + return; + var controls = [ + { + text: "BLE", + cb: function (tap) { + var on = NRF.getSecurityStatus().advertising; + if (tap) { + if (on) + NRF.sleep(); + else + NRF.wake(); + } + return on !== tap; + } + }, + { + text: "DnD", + cb: function (tap) { + var on; + if ((on = !!origBuzz)) { + if (tap) { + Bangle.buzz = origBuzz; + origBuzz = undefined; + } + } + else { + if (tap) { + origBuzz = Bangle.buzz; + Bangle.buzz = function () { return Promise.resolve(); }; + setTimeout(function () { + if (!origBuzz) + return; + Bangle.buzz = origBuzz; + origBuzz = undefined; + }, 1000 * 60 * 10); + } + } + return on !== tap; + } + }, + { + text: "HRM", + cb: function (tap) { + var _a; + var id = "widhid"; + var hrm = (_a = Bangle._PWR) === null || _a === void 0 ? void 0 : _a.HRM; + var off = !hrm || hrm.indexOf(id) === -1; + if (off) { + if (tap) + Bangle.setHRMPower(1, id); + } + else if (tap) { + Bangle.setHRMPower(0, id); + } + return !off !== tap; + } + }, + { + text: "clk", + cb: function (tap) { + if (tap) + Bangle.showClock(), terminateUI(); + return true; + }, + }, + { + text: "lch", + cb: function (tap) { + if (tap) + Bangle.showLauncher(), terminateUI(); + return true; + }, + }, + ]; + var overlay = new Overlay(); + ui = { + overlay: overlay, + ctrls: new Controls(overlay.g2, controls), + }; + ui.ctrls.draw(ui.overlay.g2); + }; + var terminateUI = function () { + state = 0; + ui === null || ui === void 0 ? void 0 : ui.overlay.hide(); + ui = undefined; + }; + var onSwipe = function () { + var _a; + switch (state) { + case 0: + case 2: + return; + case 1: + case 3: + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + }; + Bangle.prependListener('swipe', onSwipe); + var onDrag = (function (e) { + var _a, _b, _c; + var dragDistance = 30; + if (e.b === 0) + touchDown = startedUpDrag = false; + switch (state) { + case 2: + if (e.b === 0) + state = 0; + break; + case 0: + if (e.b && !touchDown) { + if (e.y <= 40) { + state = 1; + startY = e.y; + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + else { + state = 2; + } + } + break; + case 1: + if (e.b === 0) { + if (e.y > startY + dragDistance) { + initUI(); + state = 3; + startY = 0; + Bangle.prependListener("touch", onTouch); + Bangle.buzz(20); + ui.overlay.setBottom(g.getHeight()); + } + else { + terminateUI(); + break; + } + } + else { + var dragOffset = 32; + initUI(); + ui.overlay.setBottom(e.y - dragOffset); + } + (_b = E.stopEventPropagation) === null || _b === void 0 ? void 0 : _b.call(E); + break; + case 3: + (_c = E.stopEventPropagation) === null || _c === void 0 ? void 0 : _c.call(E); + if (e.b) { + if (!touchDown) { + startY = e.y; + } + else if (startY) { + var dist = Math.max(0, startY - e.y); + if (startedUpDrag || (startedUpDrag = dist > 10)) + ui.overlay.setBottom(g.getHeight() - dist); + } + } + else if (e.b === 0) { + if ((startY - e.y) > dragDistance) { + var bottom_1 = g.getHeight() - Math.max(0, startY - e.y); + if (upDragAnim) + clearInterval(upDragAnim); + upDragAnim = setInterval(function () { + if (!ui || bottom_1 <= 0) { + clearInterval(upDragAnim); + upDragAnim = undefined; + terminateUI(); + return; + } + ui.overlay.setBottom(bottom_1); + bottom_1 -= 30; + }, 50); + Bangle.removeListener("touch", onTouch); + state = 0; + } + else { + ui.overlay.setBottom(g.getHeight()); + } + } + break; + } + if (e.b) + touchDown = true; + }); + var onTouch = (function (_btn, xy) { + var _a; + if (!ui || !xy) + return; + var top = g.getHeight() - ui.overlay.height; + var left = (g.getWidth() - ui.overlay.width) / 2; + var ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top); + if (ctrl) { + onCtrlTap(ctrl, ui); + (_a = E.stopEventPropagation) === null || _a === void 0 ? void 0 : _a.call(E); + } + }); + var origBuzz; + var onCtrlTap = function (ctrl, ui) { + Bangle.buzz(20); + var col = ctrl.cb(true) ? colour.on : colour.off; + ctrl.fg = col.fg; + ctrl.bg = col.bg; + ui.ctrls.draw(ui.overlay.g2, ctrl); + }; + Bangle.prependListener("drag", onDrag); + Bangle.on("lock", terminateUI); +})(); diff --git a/apps/ctrlpad/main.ts b/apps/ctrlpad/main.ts new file mode 100644 index 000000000..5faac60fa --- /dev/null +++ b/apps/ctrlpad/main.ts @@ -0,0 +1,418 @@ +(() => { + if(!Bangle.prependListener){ + type Event = T extends `#on${infer Evt}` ? Evt : never; + + Bangle.prependListener = function( + evt: Event, + listener: () => void + ){ + // move our drag to the start of the event listener array + const handlers = (Bangle as BangleEvents)[`#on${evt}`] + + if(!handlers){ + Bangle.on(evt as any, listener); + }else{ + if(typeof handlers === "function"){ + // get Bangle to convert to array + Bangle.on(evt as any, listener); + } + + // shuffle array + (Bangle as BangleEvents)[`#on${evt}`] = [listener as any].concat( + (handlers as Array).filter((f: unknown) => f !== listener) + ); + } + }; + } + + class Overlay { + g2: Graphics; + width: number; + height: number; + + constructor() { + // x padding: 10 each side + // y top: 24, y bottom: 10 + this.width = g.getWidth() - 10 * 2; + this.height = g.getHeight() - 24 - 10; + + this.g2 = Graphics.createArrayBuffer( + this.width, + this.height, + /*bpp*/4, + { msb: true } + ); + + this.renderG2(); + } + + setBottom(bottom: number): void { + const { g2 } = this; + const y = bottom - this.height; + + Bangle.setLCDOverlay(g2, 10, y - 10); + } + + hide(): void { + Bangle.setLCDOverlay(); + } + + renderG2(): void { + this.g2 + .reset() + .setColor(g.theme.bg) + .fillRect(0, 0, this.width, this.height) + .setColor(colour.on.bg) + .drawRect(0, 0, this.width - 1, this.height - 1) + .drawRect(1, 1, this.width - 2, this.height - 2); + } + } + + type ControlCallback = (tap: boolean) => boolean | number; + type Control = { + x: number, + y: number, + fg: ColorResolvable, + bg: ColorResolvable, + text: string, + cb: ControlCallback, + }; + + const colour = { + on: { + fg: "#fff", + bg: "#00a", + }, + off: { + fg: "#000", + bg: "#bbb", + }, + } as const; + + type FiveOf = [X, X, X, X, X]; + type ControlTemplate = { text: string, cb: ControlCallback }; + + class Controls { + controls: FiveOf; + + constructor(g: Graphics, controls: FiveOf) { + // const connected = NRF.getSecurityStatus().connected; + // if (0&&connected) { + // // TODO + // return [ + // { text: "<", cb: hid.next }, + // { text: "@", cb: hid.toggle }, + // { text: ">", cb: hid.prev }, + // { text: "-", cb: hid.down }, + // { text: "+", cb: hid.up }, + // ]; + // } + + const height = g.getHeight(); + const centreY = height / 2; + const circleGapY = 30; + const width = g.getWidth(); + + this.controls = [ + { x: width / 4 - 10, y: centreY - circleGapY }, + { x: width / 2, y: centreY - circleGapY }, + { x: width * 3/4 + 10, y: centreY - circleGapY }, + { x: width / 3, y: centreY + circleGapY }, + { x: width * 2/3, y: centreY + circleGapY }, + ].map((xy, i) => { + const ctrl = xy as Control; + const from = controls[i]!; + ctrl.text = from.text; + ctrl.cb = from.cb; + Object.assign(ctrl, from.cb(false) ? colour.on : colour.off); + return ctrl; + }) as FiveOf; + } + + draw(g: Graphics, single?: Control): void { + g + .setFontAlign(0, 0) + .setFont("4x6:3" as any); + + for(const ctrl of single ? [single] : this.controls){ + g + .setColor(ctrl.bg) + .fillCircle(ctrl.x, ctrl.y, 23) + .setColor(ctrl.fg) + .drawString(ctrl.text, ctrl.x, ctrl.y); + } + } + + hitTest(x: number, y: number): Control | undefined { + let dist = Infinity; + let closest; + + for(const ctrl of this.controls){ + const dx = x-ctrl.x; + const dy = y-ctrl.y; + const d = Math.sqrt(dx*dx + dy*dy); + if(d < dist){ + dist = d; + closest = ctrl; + } + } + + return dist < 30 ? closest : undefined; + } + } + + const enum State { + Idle, + TopDrag, + IgnoreCurrent, + Active, + } + type UI = { overlay: Overlay, ctrls: Controls }; + let state = State.Idle; + let startY = 0; + let startedUpDrag = false; + let upDragAnim: IntervalId | undefined; + let ui: undefined | UI; + let touchDown = false; + + const initUI = () => { + if (ui) return; + + const controls: FiveOf = [ + { + text: "BLE", + cb: tap => { + const on = NRF.getSecurityStatus().advertising; + if(tap){ + if(on) NRF.sleep(); + else NRF.wake(); + } + return on !== tap; // on ^ tap + } + }, + { + text: "DnD", + cb: tap => { + let on; + if((on = !!origBuzz)){ + if(tap){ + Bangle.buzz = origBuzz; + origBuzz = undefined; + } + }else{ + if(tap){ + origBuzz = Bangle.buzz; + Bangle.buzz = () => Promise.resolve(); + setTimeout(() => { + if(!origBuzz) return; + Bangle.buzz = origBuzz; + origBuzz = undefined; + }, 1000 * 60 * 10); + } + } + return on !== tap; // on ^ tap + } + }, + { + text: "HRM", + cb: tap => { + const id = "widhid"; + const hrm = (Bangle as any)._PWR?.HRM as undefined | Array ; + const off = !hrm || hrm.indexOf(id) === -1; + if(off){ + if(tap) + Bangle.setHRMPower(1, id); + }else if(tap){ + Bangle.setHRMPower(0, id); + } + return !off !== tap; // on ^ tap + } + }, + { + text: "clk", + cb: tap => { + if (tap) Bangle.showClock(), terminateUI(); + return true; + }, + }, + { + text: "lch", + cb: tap => { + if (tap) Bangle.showLauncher(), terminateUI(); + return true; + }, + }, + ]; + + const overlay = new Overlay(); + ui = { + overlay, + ctrls: new Controls(overlay.g2, controls), + }; + ui.ctrls.draw(ui.overlay.g2); + }; + + const terminateUI = () => { + state = State.Idle; + ui?.overlay.hide(); + ui = undefined; + }; + + const onSwipe = () => { + switch (state) { + case State.Idle: + case State.IgnoreCurrent: + return; + + case State.TopDrag: + case State.Active: + E.stopEventPropagation?.(); + } + }; + Bangle.prependListener('swipe', onSwipe); + + const onDrag = (e => { + const dragDistance = 30; + + if (e.b === 0) touchDown = startedUpDrag = false; + + switch (state) { + case State.IgnoreCurrent: + if(e.b === 0) + state = State.Idle; + break; + + case State.Idle: + if(e.b && !touchDown){ // no need to check Bangle.CLKINFO_FOCUS + if(e.y <= 40){ + state = State.TopDrag + startY = e.y; + E.stopEventPropagation?.(); + //console.log(" topdrag detected, starting @ " + startY); + }else{ + //console.log(" ignoring this drag (too low @ " + e.y + ")"); + state = State.IgnoreCurrent; + } + } + break; + + case State.TopDrag: + if(e.b === 0){ + //console.log("topdrag stopped, distance: " + (e.y - startY)); + if(e.y > startY + dragDistance){ + //console.log("activating"); + initUI(); + state = State.Active; + startY = 0; + Bangle.prependListener("touch", onTouch); + Bangle.buzz(20); + ui!.overlay.setBottom(g.getHeight()); + }else{ + //console.log("returning to idle"); + terminateUI(); + break; // skip stopEventPropagation + } + }else{ + // partial drag, show UI feedback: + const dragOffset = 32; + + initUI(); + ui!.overlay.setBottom(e.y - dragOffset); + } + E.stopEventPropagation?.(); + break; + + case State.Active: + //console.log("stolen drag handling, do whatever here"); + E.stopEventPropagation?.(); + if(e.b){ + if(!touchDown){ + startY = e.y; + }else if(startY){ + const dist = Math.max(0, startY - e.y); + + if (startedUpDrag || (startedUpDrag = dist > 10)) // ignore small drags + ui!.overlay.setBottom(g.getHeight() - dist); + } + }else if(e.b === 0){ + if((startY - e.y) > dragDistance){ + let bottom = g.getHeight() - Math.max(0, startY - e.y); + + if (upDragAnim) clearInterval(upDragAnim); + upDragAnim = setInterval(() => { + if (!ui || bottom <= 0) { + clearInterval(upDragAnim!); + upDragAnim = undefined; + terminateUI(); + return; + } + ui.overlay.setBottom(bottom); + bottom -= 30; + }, 50) + + Bangle.removeListener("touch", onTouch); + state = State.Idle; + }else{ + ui!.overlay.setBottom(g.getHeight()); + } + } + break; + } + if(e.b) touchDown = true; + }) satisfies DragCallback; + + const onTouch = ((_btn, xy) => { + if(!ui || !xy) return; + + const top = g.getHeight() - ui.overlay.height; // assumed anchored to bottom + const left = (g.getWidth() - ui.overlay.width) / 2; // more assumptions + + const ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top); + if(ctrl){ + onCtrlTap(ctrl, ui); + E.stopEventPropagation?.(); + } + }) satisfies TouchCallback; + + let origBuzz: undefined | (() => Promise); + const onCtrlTap = (ctrl: Control, ui: UI) => { + Bangle.buzz(20); + + const col = ctrl.cb(true) ? colour.on : colour.off; + ctrl.fg = col.fg; + ctrl.bg = col.bg; + //console.log("hit on " + ctrl.text + ", col: " + ctrl.fg); + + ui.ctrls.draw(ui.overlay.g2, ctrl); + }; + + Bangle.prependListener("drag", onDrag); + Bangle.on("lock", terminateUI); + + + /* + const settings = require("Storage").readJSON("setting.json", true) as Settings || ({ HID: false } as Settings); + const haveMedia = settings.HID === "kbmedia"; + // @ts-ignore + delete settings; + + const sendHid = (code: number) => { + try{ + NRF.sendHIDReport( + [1, code], + () => NRF.sendHIDReport([1, 0]), + ); + }catch(e){ + console.log("sendHIDReport:", e); + } + }; + + const hid = haveMedia ? { + next: () => sendHid(0x01), + prev: () => sendHid(0x02), + toggle: () => sendHid(0x10), + up: () => sendHid(0x40), + down: () => sendHid(0x80), + } : null; + */ +})() diff --git a/apps/ctrlpad/metadata.json b/apps/ctrlpad/metadata.json new file mode 100644 index 000000000..273dcdd7f --- /dev/null +++ b/apps/ctrlpad/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "ctrlpad", + "name": "Control Panel", + "shortName": "ctrlpad", + "version": "0.02", + "description": "Fast access (via a downward swipe) to common functions, such as bluetooth/HRM power and Do Not Disturb", + "icon": "icon.png", + "readme": "README.md", + "type": "bootloader", + "tags": "bluetooth", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"ctrlpad.boot.js","url":"main.js"}, + {"name":"ctrlpad.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/datetime_picker/ChangeLog b/apps/datetime_picker/ChangeLog new file mode 100644 index 000000000..ef4afacd0 --- /dev/null +++ b/apps/datetime_picker/ChangeLog @@ -0,0 +1 @@ +0.01: New drag/swipe date time picker, e.g. for use with dated events alarms diff --git a/apps/datetime_picker/README.md b/apps/datetime_picker/README.md new file mode 100644 index 000000000..f602d44e1 --- /dev/null +++ b/apps/datetime_picker/README.md @@ -0,0 +1,36 @@ +# App Name + +Datetime Picker allows to swipe along the bars to select date and time elements, e.g. for the datetime of Events in the Alarm App. + +Screenshot: ![datetime with swipe controls](screenshot.png) + +## Controls + +Swipe to increase or decrease date and time elements. Press button or go back to select shown datetime. + +![datetime with numbered swipe controls](screenshot2.png) + +1. Year: swipe up to increase, down to decrease +2. Month: swipe right to increase, left to decrease +3. Day: swipe up to increase, down to decrease +4. Week: swipe up to increase week (same day next week), down to decrease (same day previous week) +5. Weekday: swipe right to increase, left to decrease (basically the same effect as 3, but with a focus on the weekday) +6. Hour: swipe right to increase, left to decrease +7. Minutes: swipe right to increase, left to decrease +8. 15 minutes: 00, 15, 30 or 45 minutes; swipe up to increase, down to decrease; wrap-around i.e. goes back to 00 after increasing from 45 + +## How to use it in code + +Sample code which would show a prompt with the number of days and hours between now and the selected datetime: + + require("datetimeinput").input().then(result => { + E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() { + load(); + }); + }); + +To set the initial value, pass a Date object named _datetime_, e.g. for today at 9:30 : + + var datetime = new Date(); + datetime.setHours(9, 30); + require("datetimeinput").input({datetime}).then(... \ No newline at end of file diff --git a/apps/datetime_picker/app-icon.js b/apps/datetime_picker/app-icon.js new file mode 100644 index 000000000..89250ff58 --- /dev/null +++ b/apps/datetime_picker/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AAnfAgf9z4FD/AFE/gFECIoFB98+tv+voFB//C/99z3Z7+J84XC3/7DpAFhKYP3AgP3AoPAOQMD/v/84LB+Z2FABiDKPoqJFKaWe/P/9Pznuf+wKB/29z+2//uTYOeTYPtRMxZKQaPAh6hBnEBwEGAoMYgHf9+/dwP5A==")) diff --git a/apps/datetime_picker/app.js b/apps/datetime_picker/app.js new file mode 100644 index 000000000..7bc66f6c5 --- /dev/null +++ b/apps/datetime_picker/app.js @@ -0,0 +1,5 @@ +require("datetimeinput").input().then(result => { + E.showPrompt(`${result}\n\n${require("time_utils").formatDuration(Math.abs(result-Date.now()))}`, {buttons:{"Ok":true}}).then(function() { + load(); + }); +}); diff --git a/apps/datetime_picker/app.png b/apps/datetime_picker/app.png new file mode 100644 index 000000000..b7cb4b46b Binary files /dev/null and b/apps/datetime_picker/app.png differ diff --git a/apps/datetime_picker/lib.js b/apps/datetime_picker/lib.js new file mode 100644 index 000000000..c3e51ae4d --- /dev/null +++ b/apps/datetime_picker/lib.js @@ -0,0 +1,145 @@ +exports.input = function(options) { + options = options||{}; + var selectedDate; + if (options.datetime instanceof Date) { + selectedDate = new Date(options.datetime.getTime()); + } else { + selectedDate = new Date(); + selectedDate.setMinutes(0); + selectedDate.setSeconds(0); + selectedDate.setMilliseconds(0); + selectedDate.setHours(selectedDate.getHours() + 1); + } + + var R; + var tip = {w: 12, h: 10}; + var arrowRectArray; + var dragging = null; + var startPos = null; + var dateAtDragStart = null; + var SELECTEDFONT = '6x8:2'; + + function drawDateTime() { + g.clearRect(R.x+tip.w,R.y,R.x2-tip.w,R.y+40); + g.clearRect(R.x+tip.w,R.y2-60,R.x2-tip.w,R.y2-40); + + g.setFont(SELECTEDFONT).setColor(g.theme.fg).setFontAlign(-1, -1, 0); + var dateUtils = require('date_utils'); + g.drawString(selectedDate.getFullYear(), R.x+tip.w+10, R.y+15) + .drawString(dateUtils.month(selectedDate.getMonth()+1,1), R.x+tip.w+65, R.y+15) + .drawString(selectedDate.getDate(), R.x2-tip.w-40, R.y+15) + .drawString(`${dateUtils.dow(selectedDate.getDay(), 1)} ${selectedDate.toLocalISOString().slice(11,16)}`, R.x+tip.w+10, R.y2-60); + } + + let dragHandler = function(event) { + "ram"; + + if (event.b) { + if (dragging === null) { + // determine which component we are affecting + var rect = arrowRectArray.find(rect => rect.y2 + ? (event.y >= rect.y && event.y <= rect.y2 && event.x >= rect.x - 10 && event.x <= rect.x + tip.w + 10) + : (event.x >= rect.x && event.x <= rect.x2 && event.y >= rect.y - tip.w - 5 && event.y <= rect.y + 5)); + if (rect) { + dragging = rect; + startPos = dragging.y2 ? event.y : event.x; + dateAtDragStart = selectedDate; + } + } + + if (dragging) { + dragging.swipe(dragging.y2 ? startPos - event.y : event.x - startPos); + drawDateTime(); + } + } else { + dateAtDragStart = null; + dragging = null; + startPos = null; + } + }; + + let catchSwipe = ()=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe listener if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(selectedDate); + }, + drag: dragHandler + }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + + R = Bangle.appRect; + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + function drawArrow(rect) { + if(rect.x2) { + g.fillRect(rect.x + tip.h, rect.y - tip.w + 4, rect.x2 - tip.h, rect.y - 4) + .fillPoly([rect.x + tip.h, rect.y, rect.x + tip.h, rect.y - tip.w, rect.x, rect.y - (tip.w / 2)]) + .fillPoly([rect.x2-tip.h, rect.y, rect.x2 - tip.h, rect.y - tip.w, rect.x2, rect.y - (tip.w / 2)]); + } else { + g.fillRect(rect.x + 4, rect.y + tip.h, rect.x + tip.w - 4, rect.y2 - tip.h) + .fillPoly([rect.x, rect.y + tip.h, rect.x + tip.w, rect.y + tip.h, rect.x + (tip.w / 2), rect.y]) + .fillPoly([rect.x, rect.y2 - tip.h, rect.x + tip.w, rect.y2 - tip.h, rect.x + (tip.w / 2), rect.y2]); + } + + } + + var yearArrowRect = {x: R.x, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setFullYear(dateAtDragStart.getFullYear() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + var weekArrowRect = {x: R.x, y: yearArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + (Math.floor(d/10) * 7)); + }}; + + var dayArrowRect = {x: R.x2 - tip.w, y: R.y, y2: R.y + (R.y2 - R.y) * 0.4, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var fifteenMinutesArrowRect = {x: R.x2 - tip.w, y: dayArrowRect.y2 + 10, y2: R.y2 - tip.w - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() - (dateAtDragStart.getMinutes() % 15) + (Math.floor(d/14) * 15)) % 60) + 60) % 60); + }}; + + var weekdayArrowRect = {x: R.x, y: R.y2, x2: (R.x2 - R.x) * 0.3 - 5, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setDate(dateAtDragStart.getDate() + Math.floor(d/10)); + }}; + + var hourArrowRect = {x: weekdayArrowRect.x2 + 5, y: R.y2, x2: weekdayArrowRect.x2 + (R.x2 - R.x) * 0.38, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setHours((((dateAtDragStart.getHours() + Math.floor(d/10)) % 24) + 24) % 24); + }}; + + var minutesArrowRect = {x: hourArrowRect.x2 + 5, y: R.y2, x2: R.x2, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMinutes((((dateAtDragStart.getMinutes() + Math.floor(d/7)) % 60) + 60) % 60); + }}; + + var monthArrowRect = {x: (R.x2 - R.x) * 0.2, y: R.y2 / 2 + 5, x2: (R.x2 - R.x) * 0.8, swipe: d => { + selectedDate = new Date(dateAtDragStart.valueOf()); + selectedDate.setMonth(dateAtDragStart.getMonth() + Math.floor(d/10)); + if (dateAtDragStart.getDate() != selectedDate.getDate()) selectedDate.setDate(0); + }}; + + arrowRectArray = [yearArrowRect, weekArrowRect, dayArrowRect, fifteenMinutesArrowRect, + weekdayArrowRect, hourArrowRect, minutesArrowRect, monthArrowRect]; + + drawDateTime(); + arrowRectArray.forEach(drawArrow); + }); +}; diff --git a/apps/datetime_picker/metadata.json b/apps/datetime_picker/metadata.json new file mode 100644 index 000000000..173e21020 --- /dev/null +++ b/apps/datetime_picker/metadata.json @@ -0,0 +1,17 @@ +{ "id": "datetime_picker", + "name": "Datetime picker", + "shortName":"Datetime picker", + "version":"0.01", + "description": "A library that allows to pick a date and time by swiping.", + "icon":"app.png", + "type":"module", + "tags":"datetimeinput", + "supports" : ["BANGLEJS2"], + "provides_modules" : ["datetimeinput"], + "readme": "README.md", + "screenshots" : [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"datetimeinput","url":"lib.js"}, + {"name":"datetime_picker.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/datetime_picker/screenshot.png b/apps/datetime_picker/screenshot.png new file mode 100644 index 000000000..6e14af8be Binary files /dev/null and b/apps/datetime_picker/screenshot.png differ diff --git a/apps/datetime_picker/screenshot2.png b/apps/datetime_picker/screenshot2.png new file mode 100644 index 000000000..9a1f9d048 Binary files /dev/null and b/apps/datetime_picker/screenshot2.png differ diff --git a/apps/elapsed_t/ChangeLog b/apps/elapsed_t/ChangeLog new file mode 100644 index 000000000..6a72c2590 --- /dev/null +++ b/apps/elapsed_t/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Handle AM/PM time in the "set target" menu. Add yesterday/today/tomorrow when showing target date to improve readability. +0.03: Add option to set clock as default, handle DST in day/month/year mode diff --git a/apps/elapsed_t/README.md b/apps/elapsed_t/README.md new file mode 100644 index 000000000..dc2173409 --- /dev/null +++ b/apps/elapsed_t/README.md @@ -0,0 +1,27 @@ +# Elapsed Time Clock +A clock that calculates the time difference between now (in blue/cyan) and any given target date (in red/orange). + +The results is show in years, months, days, hours, minutes, seconds. To save battery life, the seconds are shown only when the watch is unlocked, or can be disabled entirely. + +The time difference is positive if the target date is in the past and negative if it is in the future. + +![Screenshot 1](screenshot1.png) +![Screenshot 2](screenshot2.png) +![Screenshot 3](screenshot3.png) +![Screenshot 4](screenshot4.png) + +# Settings +## Time and date formats: +- time can be shown in 24h or in AM/PM format +- date can be shown in DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD format + +## Display years and months +You can select if the difference is shown with years, months and days, or just days. + +# TODO +- add the option to set an alarm to the target date +- add an offset to said alarm (e.g. x hours/days... before/after) + +# Author + +paul-arg [github](https://github.com/paul-arg) \ No newline at end of file diff --git a/apps/elapsed_t/app-icon.js b/apps/elapsed_t/app-icon.js new file mode 100644 index 000000000..0e9a434fc --- /dev/null +++ b/apps/elapsed_t/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcA/4A/AH8kyVJARAQE/YRLn4RD/IRT5cs2QCEEbQgFAQYjIrMlAQwjR5JHIsv2pNkz3JsgjKl/yEAO/I5l/+REBz/7I5f/EYf/I5Vf//2rNlz//8gjJAgIjE/hHIy7xEAAQjIDoIAG+RHHCA///wjHCJIjHMoI1HEY+zCI6zJv4dCFIX9R5PPR4vsEZNJCILXC/77JyXLn4jD/b7KpMnI4fZBARHHpcsEYW2AQIjKARBHIDoICECJIjRpZKCAQYjbCMH/CJVLCAgA/AHYA==")) diff --git a/apps/elapsed_t/app.js b/apps/elapsed_t/app.js new file mode 100644 index 000000000..13fbca2cd --- /dev/null +++ b/apps/elapsed_t/app.js @@ -0,0 +1,494 @@ +const APP_NAME = "elapsed_t"; + +//const COLOUR_BLACK = 0x0; +//const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25) +const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5) +const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75) +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1) +//const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +//const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) +//const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +//const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) +const COLOUR_CYAN = "#00FFFF"; +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) + +const SCREEN_WIDTH = g.getWidth(); +const SCREEN_HEIGHT = g.getHeight(); +const BIG_FONT_SIZE = 38; +const SMALL_FONT_SIZE = 22; + +var arrowFont = atob("BwA4AcAOAHADgBwA4McfOf3e/+P+D+A+AOA="); // contains only the > character + +var now = new Date(); + +var settings = Object.assign({ + // default values + displaySeconds: true, + displayMonthsYears: true, + dateFormat: 0, + time24: true +}, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + +var temp_displaySeconds = settings.displaySeconds; + +var data = Object.assign({ + // default values + target: { + isSet: false, + Y: now.getFullYear(), + M: now.getMonth() + 1, // Month is zero-based, so add 1 + D: now.getDate(), + h: now.getHours(), + m: now.getMinutes(), + s: 0 + } +}, require('Storage').readJSON(APP_NAME + ".data.json", true) || {}); + +function writeData() { + require('Storage').writeJSON(APP_NAME + ".data.json", data); +} + +function writeSettings() { + require('Storage').writeJSON(APP_NAME + ".settings.json", settings); + temp_displaySeconds = settings.temp_displaySeconds; +} + +let inMenu = false; + +Bangle.on('touch', function (zone, e) { + if (!inMenu && e.y > 24) { + if (drawTimeout) clearTimeout(drawTimeout); + E.showMenu(menu); + inMenu = true; + } +}); + +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +function formatDateTime(date, dateFormat, time24, showSeconds) { + var formattedDateTime = { + date: "", + time: "" + }; + + var DD = pad2(date.getDate()); + var MM = pad2(date.getMonth() + 1); // Month is zero-based + var YYYY = date.getFullYear(); + var h = date.getHours(); + var hh = pad2(date.getHours()); + var mm = pad2(date.getMinutes()); + var ss = pad2(date.getSeconds()); + + switch (dateFormat) { + case 0: + formattedDateTime.date = `${DD}/${MM}/${YYYY}`; + break; + + case 1: + formattedDateTime.date = `${MM}/${DD}/${YYYY}`; + break; + + case 2: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + + default: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + } + + if (time24) { + formattedDateTime.time = `${hh}:${mm}${showSeconds ? `:${ss}` : ''}`; + } else { + var ampm = (h >= 12 ? 'PM' : 'AM'); + var h_ampm = h % 12; + h_ampm = (h_ampm == 0 ? 12 : h_ampm); + formattedDateTime.time = `${h_ampm}:${mm}${showSeconds ? `:${ss}` : ''} ${ampm}`; + } + + return formattedDateTime; +} + +function formatHourToAMPM(h){ + var ampm = (h >= 12 ? 'PM' : 'AM'); + var h_ampm = h % 12; + h_ampm = (h_ampm == 0 ? 12 : h_ampm); + return `${h_ampm} ${ampm}` +} + +function howManyDaysInMonth(month, year) { + return new Date(year, month, 0).getDate(); +} + +function handleExceedingDay() { + var maxDays = howManyDaysInMonth(data.target.M, data.target.Y); + menu.Day.max = maxDays; + if (data.target.D > maxDays) { + menu.Day.value = maxDays; + data.target.D = maxDays; + } +} + +var menu = { + "": { + "title": "Set target", + back: function () { + E.showMenu(); + Bangle.setUI("clock"); + inMenu = false; + draw(); + } + }, + 'Day': { + value: data.target.D, + min: 1, max: 31, wrap: true, + onchange: v => { + data.target.D = v; + } + }, + 'Month': { + value: data.target.M, + min: 1, max: 12, noList: true, wrap: true, + onchange: v => { + data.target.M = v; + handleExceedingDay(); + } + }, + 'Year': { + value: data.target.Y, + min: 1900, max: 2100, + onchange: v => { + data.target.Y = v; + handleExceedingDay(); + } + }, + 'Hours': { + value: data.target.h, + min: 0, max: 23, wrap: true, + onchange: v => { + data.target.h = v; + }, + format: function (v) {return(settings.time24 ? pad2(v) : formatHourToAMPM(v))} + }, + 'Minutes': { + value: data.target.m, + min: 0, max: 59, wrap: true, + onchange: v => { + data.target.m = v; + }, + format: function (v) { return pad2(v); } + }, + 'Seconds': { + value: data.target.s, + min: 0, max: 59, wrap: true, + onchange: v => { + data.target.s = v; + }, + format: function (v) { return pad2(v); } + }, + 'Save': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(true); + writeSettings(); + temp_displaySeconds = settings.displaySeconds; + updateQueueMillis(settings.displaySeconds); + draw(); + }, + 'Reset': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(false); + updateQueueMillis(settings.displaySeconds); + draw(); + }, + 'Set clock as default': function () { + setClockAsDefault(); + E.showAlert("Elapsed Time was set as default").then(function() { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + draw(); + }); + } +}; + +function setClockAsDefault(){ + let storage = require('Storage'); + let settings = storage.readJSON('setting.json',true)||{clock:null}; + settings.clock = "elapsed_t.app.js"; + storage.writeJSON('setting.json', settings); +} + +function setTarget(set) { + if (set) { + target = new Date( + data.target.Y, + data.target.M - 1, + data.target.D, + data.target.h, + data.target.m, + data.target.s + ); + data.target.isSet = true; + } else { + target = new Date(); + Object.assign( + data, + { + target: { + isSet: false, + Y: now.getFullYear(), + M: now.getMonth() + 1, // Month is zero-based, so add 1 + D: now.getDate(), + h: now.getHours(), + m: now.getMinutes(), + s: 0 + } + } + ); + menu.Day.value = data.target.D; + menu.Month.value = data.target.M; + menu.Year.value = data.target.Y; + menu.Hours.value = data.target.h; + menu.Minutes.value = data.target.m; + menu.Seconds.value = 0; + } + + writeData(); +} + +var target; +setTarget(data.target.isSet); + +var drawTimeout; +var queueMillis = 1000; + + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + var delay = queueMillis - (Date.now() % queueMillis); + if (queueMillis == 60000 && signIsNegative()) { + delay += 1000; + } + + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw(); + }, delay); +} + +function updateQueueMillis(displaySeconds) { + if (displaySeconds) { + queueMillis = 1000; + } else { + queueMillis = 60000; + } +} + +Bangle.on('lock', function (on, reason) { + if (inMenu) { // if already in a menu, nothing to do + return; + } + + if (on) { // screen is locked + temp_displaySeconds = false; + updateQueueMillis(false); + draw(); + } else { // screen is unlocked + temp_displaySeconds = settings.displaySeconds; + updateQueueMillis(temp_displaySeconds); + draw(); + } +}); + +function signIsNegative() { + var now = new Date(); + return (now < target); +} + +function diffToTarget() { + var diff = { + sign: "+", + Y: "0", + M: "0", + D: "0", + hh: "00", + mm: "00", + ss: "00" + }; + + if (!data.target.isSet) { + return (diff); + } + + var now = new Date(); + diff.sign = now < target ? '-' : '+'; + + if (settings.displayMonthsYears) { + var start; + var end; + + if (now > target) { + start = target; + end = now; + } else { + start = now; + end = target; + } + + diff.Y = end.getFullYear() - start.getFullYear(); + diff.M = end.getMonth() - start.getMonth(); + diff.D = end.getDate() - start.getDate(); + diff.hh = end.getHours() - start.getHours(); + diff.mm = end.getMinutes() - start.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset(); + diff.ss = end.getSeconds() - start.getSeconds(); + + // Adjust negative differences + if (diff.ss < 0) { + diff.ss += 60; + diff.mm--; + } + if (diff.mm < 0) { + diff.mm += 60; + diff.hh--; + } + if (diff.hh < 0) { + diff.hh += 24; + diff.D--; + } + if (diff.D < 0) { + var lastMonthDays = new Date(end.getFullYear(), end.getMonth(), 0).getDate(); + diff.D += lastMonthDays; + diff.M--; + } + if (diff.M < 0) { + diff.M += 12; + diff.Y--; + } + + + } else { + var timeDifference = target - now; + timeDifference = Math.abs(timeDifference); + + // Calculate days, hours, minutes, and seconds + diff.D = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + diff.hh = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + diff.mm = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)); + diff.ss = Math.floor((timeDifference % (1000 * 60)) / 1000); + } + + // add zero padding + diff.hh = pad2(diff.hh); + diff.mm = pad2(diff.mm); + diff.ss = pad2(diff.ss); + + return diff; +} + +function draw() { + var now = new Date(); + var nowFormatted = formatDateTime(now, settings.dateFormat, settings.time24, temp_displaySeconds); + var targetFormatted = formatDateTime(target, settings.dateFormat, settings.time24, true); + var diff = diffToTarget(); + + const nowY = now.getFullYear(); + const nowM = now.getMonth(); + const nowD = now.getDate(); + + const targetY = target.getFullYear(); + const targetM = target.getMonth(); + const targetD = target.getDate(); + + var diffYMD; + if (settings.displayMonthsYears) + diffYMD = `${diff.sign}${diff.Y}Y ${diff.M}M ${diff.D}D`; + else + diffYMD = `${diff.sign}${diff.D}D`; + + var diff_hhmm = `${diff.hh}:${diff.mm}`; + + g.clearRect(0, 24, SCREEN_WIDTH, SCREEN_HEIGHT); + //console.log("drawing"); + + let y = 24; //Bangle.getAppRect().y; + + // draw current date + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_CYAN : COLOUR_BLUE); + g.drawString(nowFormatted.date, 4, y); + y += SMALL_FONT_SIZE; + + // draw current time + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_CYAN : COLOUR_BLUE); + g.drawString(nowFormatted.time, 4, y); + y += SMALL_FONT_SIZE; + + // draw arrow + g.setFontCustom(arrowFont, 62, 16, 13).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString(">", 4, y + 3); + + if (data.target.isSet) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + + if (nowY == targetY && nowM == targetM && nowD == targetD) { + // today + g.drawString("TODAY", 4 + 16 + 6, y); + } else if (nowY == targetY && nowM == targetM && nowD - targetD == 1) { + // yesterday + g.drawString("YESTERDAY", 4 + 16 + 6, y); + } else if (nowY == targetY && nowM == targetM && targetD - nowD == 1) { + // tomorrow + g.drawString("TOMORROW", 4 + 16 + 6, y); + } else { + // general case + // draw target date + g.drawString(targetFormatted.date, 4 + 16 + 6, y); + } + + y += SMALL_FONT_SIZE; + + // draw target time + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString(targetFormatted.time, 4, y); + y += SMALL_FONT_SIZE + 4; + + } else { + // draw NOT SET + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_ORANGE : COLOUR_RED); + g.drawString("NOT SET", 4 + 16 + 6, y); + y += 2 * SMALL_FONT_SIZE + 4; + } + + // draw separator + g.setColor(g.theme.fg); + g.drawLine(0, y - 4, SCREEN_WIDTH, y - 4); + + // draw diffYMD + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(0, -1).setColor(g.theme.fg); + g.drawString(diffYMD, SCREEN_WIDTH / 2, y); + y += SMALL_FONT_SIZE; + + // draw diff_hhmm + g.setFont("Vector", BIG_FONT_SIZE).setFontAlign(0, -1).setColor(g.theme.fg); + g.drawString(diff_hhmm, SCREEN_WIDTH / 2, y); + + // draw diff_ss + if (temp_displaySeconds) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_GREY); + g.drawString(diff.ss, SCREEN_WIDTH / 2 + 52, y + 13); + } + + queueDraw(); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setUI("clock"); + +draw(); diff --git a/apps/elapsed_t/app.png b/apps/elapsed_t/app.png new file mode 100644 index 000000000..c2cac4fa1 Binary files /dev/null and b/apps/elapsed_t/app.png differ diff --git a/apps/elapsed_t/metadata.json b/apps/elapsed_t/metadata.json new file mode 100644 index 000000000..fa0674e0b --- /dev/null +++ b/apps/elapsed_t/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "elapsed_t", + "name": "Elapsed Time Clock", + "shortName": "Elapsed Time", + "type": "clock", + "version":"0.03", + "description": "A clock that calculates the time difference between now and any given target date.", + "tags": "clock,tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"elapsed_t.app.js","url":"app.js"}, + {"name":"elapsed_t.settings.js","url":"settings.js"}, + {"name":"elapsed_t.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"elapsed_t.data.json"}], + "icon": "app.png", + "readme": "README.md", + "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }, { "url": "screenshot3.png" }, { "url": "screenshot4.png" }], + "allow_emulator":true +} diff --git a/apps/elapsed_t/screenshot1.png b/apps/elapsed_t/screenshot1.png new file mode 100644 index 000000000..d15a5a9ae Binary files /dev/null and b/apps/elapsed_t/screenshot1.png differ diff --git a/apps/elapsed_t/screenshot2.png b/apps/elapsed_t/screenshot2.png new file mode 100644 index 000000000..00ad8aa36 Binary files /dev/null and b/apps/elapsed_t/screenshot2.png differ diff --git a/apps/elapsed_t/screenshot3.png b/apps/elapsed_t/screenshot3.png new file mode 100644 index 000000000..8ca6212f6 Binary files /dev/null and b/apps/elapsed_t/screenshot3.png differ diff --git a/apps/elapsed_t/screenshot4.png b/apps/elapsed_t/screenshot4.png new file mode 100644 index 000000000..e2a10ab62 Binary files /dev/null and b/apps/elapsed_t/screenshot4.png differ diff --git a/apps/elapsed_t/settings.js b/apps/elapsed_t/settings.js new file mode 100644 index 000000000..d3a7cb357 --- /dev/null +++ b/apps/elapsed_t/settings.js @@ -0,0 +1,55 @@ +(function(back) { + var APP_NAME = "elapsed_t"; + var FILE = APP_NAME + ".settings.json"; + // Load settings + var settings = Object.assign({ + // default values + displaySeconds: true, + displayMonthsYears: true, + dateFormat: 0, + time24: true + }, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]; + + // Show the menu + E.showMenu({ + "" : { "title" : "Elapsed Time" }, + "< Back" : () => back(), + 'Show\nseconds': { + value: !!settings.displaySeconds, + onchange: v => { + settings.displaySeconds = v; + writeSettings(); + } + }, + 'Show months/\nyears': { + value: !!settings.displayMonthsYears, + onchange: v => { + settings.displayMonthsYears = v; + writeSettings(); + } + }, + 'Time format': { + value: !!settings.time24, + onchange: v => { + settings.time24 = v; + writeSettings(); + }, + format: function (v) {return v ? "24h" : "AM/PM";} + }, + 'Date format': { + value: settings.dateFormat, + min: 0, max: 2, wrap: true, + onchange: v => { + settings.dateFormat = v; + writeSettings(); + }, + format: function (v) {return dateFormats[v];} + } + }); +}) diff --git a/apps/fileman/manage_files.html b/apps/fileman/manage_files.html new file mode 100644 index 000000000..30726a869 --- /dev/null +++ b/apps/fileman/manage_files.html @@ -0,0 +1,101 @@ + + + + + + + + +
+ + + +
Stats
{{s[0]}}{{s[1]}}
+

Files

+
+ + +
+ + + + + + + + + +
Filenameshowdelete
{{file}}
+
+ +
+
+ + + + diff --git a/apps/fontkorean/ChangeLog b/apps/fontkorean/ChangeLog new file mode 100644 index 000000000..e595c00da --- /dev/null +++ b/apps/fontkorean/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Corrected formatting of punctuation \ No newline at end of file diff --git a/apps/fontkorean/README.md b/apps/fontkorean/README.md new file mode 100644 index 000000000..6d9eecd45 --- /dev/null +++ b/apps/fontkorean/README.md @@ -0,0 +1,17 @@ +# Fonts (Korean) + +This library provides an Korean font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +Korean characters from Unicode codepoint 32-255, 0x1100-0x11FF, 0x3130-0x318F, 0xA960-0xA97F + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating fontkorean.pbf + +* Go to `bin` directory +* Run `./font_creator.js "Korean" ../apps/fontkorean/font.pbf` diff --git a/apps/fontkorean/app.png b/apps/fontkorean/app.png new file mode 100644 index 000000000..a4b02ea3a Binary files /dev/null and b/apps/fontkorean/app.png differ diff --git a/apps/fontkorean/boot.js b/apps/fontkorean/boot.js new file mode 100644 index 000000000..5f3a24433 --- /dev/null +++ b/apps/fontkorean/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontkorean.pbf")); }; \ No newline at end of file diff --git a/apps/fontkorean/font.pbf b/apps/fontkorean/font.pbf new file mode 100644 index 000000000..8a9e44051 Binary files /dev/null and b/apps/fontkorean/font.pbf differ diff --git a/apps/fontkorean/lib.js b/apps/fontkorean/lib.js new file mode 100644 index 000000000..bcb3b4bca --- /dev/null +++ b/apps/fontkorean/lib.js @@ -0,0 +1,3 @@ +exports.getFont = (options) => { + return "Intl"; // placeholder for now - see https://github.com/espruino/BangleApps/issues/3109 +}; \ No newline at end of file diff --git a/apps/fontkorean/metadata.json b/apps/fontkorean/metadata.json new file mode 100644 index 000000000..2ed0c7545 --- /dev/null +++ b/apps/fontkorean/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fontkorean", + "name": "Fonts (Korean)", + "version":"0.02", + "description": "Installs a font data, Unifont characters for Korean **Requires 420 KB storage**", + "icon": "app.png", + "tags": "font,fonts,language", + "type": "module", + "provides_modules" : ["font"], + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"font","url":"lib.js"}, + {"name":"fontkorean.boot.js","url":"boot.js"}, + {"name":"fontkorean.pbf","url":"font.pbf"} + ] +} diff --git a/apps/jclock/ChangeLog b/apps/jclock/ChangeLog new file mode 100644 index 000000000..c530930a6 --- /dev/null +++ b/apps/jclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Created +0.02: Changed side bar color to blue for better clarity when it's locked diff --git a/apps/jclock/README.md b/apps/jclock/README.md new file mode 100644 index 000000000..7ddb346dc --- /dev/null +++ b/apps/jclock/README.md @@ -0,0 +1,26 @@ +# jclock + +I have used Rebble clock since I bought my Banglejs 2, and wanted to make my own clock with much simpler features and to switch the time window and the feature window because I'm wearing my watch on my left wrist and about a half (left side) of the screen is covered by the sleeve of my jacket or shirts. Of course it won't happen during summer, but I decided to make my first Bagle app with these changes. See Features below for the items displayed on the screen. +- The layout is inspired by the Rebble clock. +- The big font KdamThmor is copied from the Rebble clock. + +## Features +- Single screen +- No settings +- Time on the right side with big font +- On the sidebar on the left + - Day of week + - Day + - Month + - Steps + - Bluetooth connection status + - Battery % +- Update time and status every 1 minute + +## Screenshots +![](jclock_screenshot_no_BT.png) +![](jclock_screenshot_BT.png) + +## Creator + +Written by [JeonLab](https://jeonlab.wordpress.com) diff --git a/apps/jclock/app-icon.js b/apps/jclock/app-icon.js new file mode 100644 index 000000000..7744de49f --- /dev/null +++ b/apps/jclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/AAIHB7ue4cYrPO0cQtUy2WUHU0kyVJARAQEhIRLkgQCgQOKAQWACIYbHIImQAYMSpQRLgmSCIVSCJcACIWSKJARPzO9gETm+4BwNACI8Etu28GN23fIgIRIg14hnw/UI7wRGltvCIMjuEO7BCBCI97twRCsEICIMO7gRKnYRCju2/A1Hr4RHEY5ZDGokJzeACJRZB+EAgPbCIxrDgd4g347kBEY8rLIUHUIPA9qhICIcA/LFBj830ARLAAwR/CMkd3wOBozXHCIcE5oRCswRLg/RCIMD3gRLcoIRBgOwCJ8Z+ARIfYQRDx8ACI97CI1uCI9K7YRFglt23b4ARGuQROpPXAQI1EAAJHHkgCCg4gB+ARVyQRIdI+SdgVSCKFKCJcEyADBgVJlIRJhMkCIUAEQgCIwAXECJUgHYoRJPQIAlA=")) diff --git a/apps/jclock/app.js b/apps/jclock/app.js new file mode 100644 index 000000000..5c4d709d6 --- /dev/null +++ b/apps/jclock/app.js @@ -0,0 +1,68 @@ +// single screen, clock on the right, sidebar without image (date/steps/bluetooth connection status/batt%) + +// Large font KdamThmor taken from Rebble clock +Graphics.prototype.setFontKdamThmor = function(scale) { + // Actual height 70 (69 - 0) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AH4AMgfABZM/BZMB/4WJg/+BZMf/ALJ//gIpP/wAugLpUAvyBKsDC/ACKYJQIKYJgaYKv6YJh7HJeoP8VxLSJg//+D0JIhMf/7RIf4JPJv//LX5a6CwLvJn5aJLYIKJgY4IADn/KpKvBAAKvIAARiGBQanGOwILJBQgLFFogvGIgZHGWAIAEdwg5FNYreBAAjvDeoIAFYQcfBYy3DEQRKEKQQiCAoRiCIogoDCIJGDEQLlEIwZoBCwYLCHQQoBQwgGEj7aFGoKuDKwYSFE4LZFv41Ch6dEIITICn5FEDwQuDeAwuEBQgeEB4b8EFwbADNIZdaHQoSBFwUfNIoGEv5GFXYpGEIoJBCZgjZGHQILDCwIpDj//GgQoBMggcBAApkDBQwiDDoQAEEQY0BERJGBERBGCERC8BBYrYFBQj8FLwrBGBQbkFEYoKFBYgtFL4jLFZ4gKJAH4AciALKRA73DbIgAFj/ABZLOGEQjDEj40En6tEv4oDgLPEAoLRFCIcHDgouJDgP4FxAiFFwt//xXEFwcDEQouEj4iEFwv/EQguEEQJ6EFwgiBS4guE/5uEFwiiBAAyiDBQwdDCw4uCIoIAGFwSLBF34unAAy7EAAy7EAAzqEAArqEF34ukAH4AGgfgNJWAAod8Cwn+SQn4RggFEv4oE/4FDg//FAYFFn4oEAoidBFAYFFh//YIYFBFwd//7BDAoIuCgf/YIYFBFwcfFAgFFDgIoDDgIFCEQpcBFwZFFn4uEAoJcEFwYFBLgouDQoo/BAwcf/hcEFwgiELgPfFwQRBEQYVBFwcPDYYzB+YSDn55DKwOPFwgbCKwP8CQYuBXIouEKIZcBIIgbF/BBEDYZcB4ASFDYI5BCgIuEHQSzCFwo6CeYQuEv4nBOYIPBFwa7Ddoa7FJoLtCFwhNBAAQfBFwiTBAAXAT4oKDCYSfFAAQ9BFwg6BAAQHBFwhDCLgQuFIwY5BFwhGDDwT9FOQI5CFwpSDDoYuDBYQWCFwoLCAgQuFCIsHFwgAFh4uEAH4AWjgLKvwGFj6LDP4sBcgjhCCwaGDn4LEgKjDAgKXEh61Dg7LEdQIuDj7AEZgIpDfYPACIgdCFwLjDdIQRCFwIoDEQJdEFAgiBJgYoEEQoLCAoRFFBYRjCFAIWDQII0Dv6SFv40CRYg1DHQRXBBQg1BFISpDBwQSEEQTQDj4SCDYJKBh42Cv4uCh4TCn4aBIIIuDCYIHBDQIeBFwYPBg4aCe4YPDfAYuHv4uNLo6bBLpJ4EFwYTBEQIHBCQYbBHQIqBEwIGCXYl/IQTwDD4P+CwIfBFILCCBAQACwACBEQQQBAArlDn4LGcoY3BGAIlEHQYAB+YiGMQIAB54DCOgRGD/0fEQpGD+A+CEQZ6BLYhFEKQX8HwYKDBYXgHwQ5DBYQpBBYQ5DHYRWDUQQAGgK5DADsBBZUfb4IAIOYoAETgJcFAAbLBBRBoBUQg5FRYxQDRYJGIZQQ5KFxDtCFxDpCFw7dIfAouICwQuHHIP+FxBQB8YuHf4UPFw6KCn4uGKAWAFw6KB/glBHJHAFw5QCQQIuGRQLzBFww5CKgRQH/A9BFwxQCFw45BCYQuGKAI5BFwwGBKAIuHRQRVCFwhQDFw6KBKAIuHfwQAEGAYKGGgbQCAAowCFwIAGF34ugAAjqHTojqFfQrqFcYoWJF0f+CxMH8ALJAEkCBZU8BRMB/CCKOw0DA4V/OwqhBA4IDBwAKFVoTlBBQytCn6xDBQX/IQQDDAgIACSwIRBTQQWDGwUHHQYzBAAK5CHQk/Fwo6EFwppBNoQuGgIPDFwYeCOoguC34eCh74DEASMCCQI+CDYQCBCQYuDDYMPFwQ6BFwYbBn4uCg4uE8ASBFwUfFwqIBCQV/FwsfLpAbBPgZdFFwpdGFwhdHDwQPELoYeCHwYbD/46CAYaMEBwLqFFwRGCv5RDFYUfBYIWBGQQuDv7iDMIQuCNIIADCwQuCfIgiDFwT5DEQYuDHQIiFVAc/EQyJDIwYiDc4RGDNAYuBCAJGDRYQHBCAQLDCwcPCAR+BHIgAEBYQKHEYQtDAH4Ak/gKJZALMBRhLGDAAjSGWYgLCEY7qDBYwtCXhBEBewzpF/5fGj4LDdYwKD//gKBBeHKAZGGHIX+gJGGKAQfBHQoSBCYQEB+A5GA4InBHQiJEQgKKGOIUPHQg5CFQU/HQaKDVgR1ERQQeCIwK8DBQPvDwUHFwZQB/0/DwUfFwaKB+IeDv4PCHIWHFw45B/geDFwjBCDwYPDEQKsCLoxFB+CIDCQIPCP4OAj6MCj4uEBAN/FQV/SAS0CFwIqBXYioCA4ZYBVwYbBHoIaCQAY+CHoPACwKADGwa+CEQcPFQIfBAARVCgE+dgiGCBYRVCHQLiFganEEQsIZQgiFAAZFGAAZGDNAYADcQSLDAAhSCVwYLHHI4LCCxC5FAH4AIJhRYBXgQAGh5vJgE/VI4uDSRAuJoAuJg4uKvguJg/wFxN/OAQuGaoIuJv/8FxAWBFxN/T4YuFCwIuJCwIuICwQuICwIuICwQGDFwgWCEQQuECwQpDFwk/BQIdDFwYPBCwguECwwuDCw4uDCw4uCCw4uDCw4uCCxAuCCxAuBCwYKEFwQWCRIYuD8YWIEAO/CxEPCoQWGLQYWHFwIWJJ4YWHFwYKGFwYWHFwYKHFwQWIFwQKHFwQWIFwQKIFwIWJdQQuJ8ALJAH8f/BuK/gIFv6RDBYqlBwEBSIIjFA4OAWgSSEA4WAv4LGA4TXC//Ab4v+j4LCwBYDAwP8DQTNEAwXzAYTCDFQfvAYRSDFQYADIwYqDAAZGCEQYAB8A6ENARHCDoI6DAgKKCD4N/HQQIB8ACBCYQGBAYMHE4IxBIQIPBHQU/DYIOBA4ISCDYQHBh4iCh7ICD4IaEAYJpCB4d/GwQuEGwasBDwYPBA4MHFw4HCj4uHA4QuULqyUDRgxCCRhC0Cn46CEwYbB+DhCYQa7DAAQyBcoIaBdQoLBawYrCAApRCHQILGKIT/C//7Eoh1DAAPvAYRRCIwkfEQpGD/AyDBQSBBCQQiGKQX+HwYiDKQXwGQRFDBYYyDNAYLCAwILCBQg+FHIgAEC4IKIQwKtCAH4AWnwKJPoKrEOAi3GaY4WJ/6KHW4ShIfwTbFAAMDCwX8A4UYHIrQE8AiFeYcHHwQiDKQZ6DEQZSCgYmDEQZGCj4uCEQQZBCYRtDNAPAg46Cg5hDv5aBBYI6Bn4aCRYInBDQIpCFwQTBGwQaBGQIuCn59Cn4uBSAgbDHoYuCE4JlCEwJjBCQUPEQUH/hjCFwaUCj/wHIKzDSgd/4AWBQAhhDcYTpDFwg5BUYYuE8Y5ELoufHIhdFaoguBYYbJESgjWDGgQHCH4IiDBQZZBCIIiCKAa7CIwIWCKAbPC8AWCKAZpCCgRQFIQhQGHQQADKAhOEKApGDAARQEIwZQHIwpQFBYpQFKQgWHPwYWHBYQWIEYREGL4YKJAH4AegIEDsCxGPIfgCwr/Dn6nFh6jCgKcGn/wEQQbDXgYqCn/4BQkDDwYPDFzV/JoUfB4RdOgI1DnjG/ACoA='))), + 46, + atob("GBo2NjY2NjY2NjY2Gg=="), + 94+(scale<<8)+(1<<16) + ); + return this; +}; + +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)); +} + +const zeroPad = (num, places) => String(num).padStart(places, '0'); + +function draw() { + let barWidth = 64; + let date = new Date(); + + // queue next draw in one minute + queueDraw(); + + // clean screen + g.reset().clearRect(Bangle.appRect); + + // draw side bar in blue + g.setColor('#00f'); + g.fillRect(0, 0, barWidth, g.getHeight()); + + // show time on the right + g.setColor(g.theme.fg); + g.setFontKdamThmor().setFontAlign(0,-1).drawString(zeroPad(date.getHours(),2), 120, 10); + g.setFontKdamThmor().setFontAlign(0,-1).drawString(zeroPad(date.getMinutes(),2), 120, g.getHeight()/2+10); + + // show date + g.setFont('Vector', 20).setFontAlign(0, -1).setColor('#fff'); + g.drawString(require("date_utils").dow(date.getDay(),1).toUpperCase(), barWidth/2, 3); + g.drawString(date.getDate(), barWidth/2, 28); + g.drawString(require("date_utils").month(date.getMonth()+1,1).toUpperCase(), barWidth/2, 53); + + // divider, place holder for any other info + g.drawString('=====', barWidth/2, 78); + + // show daily steps + g.drawString(Bangle.getHealthStatus("day").steps, barWidth/2, 103); + + // show battery remaining percentage + g.drawString(E.getBattery() + '%', barWidth/2, 153); + + // Bluetooth connection status + if (NRF.getSecurityStatus().connected) g.drawString('>BT<', barWidth/2, 128); +} + +draw(); + +Bangle.setUI("clock"); diff --git a/apps/jclock/app.png b/apps/jclock/app.png new file mode 100644 index 000000000..a0bf26562 Binary files /dev/null and b/apps/jclock/app.png differ diff --git a/apps/jclock/jclock_screenshot_BT.png b/apps/jclock/jclock_screenshot_BT.png new file mode 100644 index 000000000..e4eac99d0 Binary files /dev/null and b/apps/jclock/jclock_screenshot_BT.png differ diff --git a/apps/jclock/jclock_screenshot_no_BT.png b/apps/jclock/jclock_screenshot_no_BT.png new file mode 100644 index 000000000..a312d23ed Binary files /dev/null and b/apps/jclock/jclock_screenshot_no_BT.png differ diff --git a/apps/jclock/metadata.json b/apps/jclock/metadata.json new file mode 100644 index 000000000..9a7ddd337 --- /dev/null +++ b/apps/jclock/metadata.json @@ -0,0 +1,16 @@ +{ "id":"jclock", + "name":"jclock", + "shortName":"jclock", + "icon":"app.png", + "version":"0.02", + "description":"Similar layout to Rebble clock, but much simpler features with switched time and the feature window. The time is on the right side. This is my first Bangle app.", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"jclock_screenshot_no_BT.png"},{"url":"jclock_screenshot_BT.png"}], + "storage": [ + {"name":"jclock.app.js","url":"app.js"}, + {"name":"jclock.img","url":"app-icon.js","evaluate":true} + ], + "readme":"README.md" +} diff --git a/apps/measuretime/ChangeLog b/apps/measuretime/ChangeLog new file mode 100644 index 000000000..81fba8e15 --- /dev/null +++ b/apps/measuretime/ChangeLog @@ -0,0 +1 @@ +0.1: Initial release \ No newline at end of file diff --git a/apps/measuretime/README.md b/apps/measuretime/README.md new file mode 100644 index 000000000..78d04f30d --- /dev/null +++ b/apps/measuretime/README.md @@ -0,0 +1,7 @@ +# Measure Time + +Measure time in a fancy way. Inspired by a Watchface I had on my first Pebble Watch. + +Written by [prefectAtEarth](https://www.github.com/prefectAtEarth/) + +![](measuretime.png) \ No newline at end of file diff --git a/apps/measuretime/measuretime-icon.js b/apps/measuretime/measuretime-icon.js new file mode 100644 index 000000000..4592548a7 --- /dev/null +++ b/apps/measuretime/measuretime-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/AAIHB/fe8EHrvv333xVS221jnnlFC7//9NP997zXWjHGn+EGJsu9wAC0AHBgugq99C5d0kUq1WtoAHBgnaw8nC5d9mdwgEN7QHBxvQ5nhGwQXNiQHB19A41xC5dy3YXCwAHBwkqx3tI5d3AAV8L4UIDYRkBogADpTOQhWqAAZOLAAuoxAABfyYXXI4pKRO4oACqBHl0QXWAC8IF4QABwpHRkUilALHgutvwvMBY8NoEHKakCqtHR5gAH1FY7wUFcYS/LI5Fwd4r7IqXuJ4uUAYMK1QABKhEKIAQAC1kW7SnDAAUlPxnBiN9xEnu93vx6KAAeHyMdI5wAGox3OS5GAU4oAEoAXJhTXGfigAWhAvWX6QvcT5nog5HJF5QXLX5AAC0levwXId5cNoAvJhWqAAILHgVAhxHMQaZfFwoXQI5YALO5ZHPC6bXDAAmADqYARhBHXkUilC/oA=")) \ No newline at end of file diff --git a/apps/measuretime/measuretime.app.js b/apps/measuretime/measuretime.app.js new file mode 100644 index 000000000..c7865bffe --- /dev/null +++ b/apps/measuretime/measuretime.app.js @@ -0,0 +1,180 @@ +{ + require("Font7x11Numeric7Seg").add(Graphics); + g.setFont("7x11Numeric7Seg"); + g.setFontAlign(0, 0); + + const centerY = g.getHeight() / 2; //88 + const lineStart = 25; + const lineEndFull = 110; + const lineEndHalf = 90; + const lineEndQuarter = 70; + const lineEndDefault = 50; + + let drawTimeout; + + let queueDrawTime = function () { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + drawTime(); + }, 60000 - (Date.now() % 60000)); + }; + + let drawCenterLine = function () { + // center line + g.drawLineAA(0, centerY, g.getWidth(), centerY); + // left decoration + var steps = [0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + var stepsReversed = steps.slice(); + stepsReversed.reverse(); + var polyLeftTop = []; + var polyLeftBottom = []; + var polyRightTop = []; + var polyRightBottom = []; + let xL = 0; + let xR = g.getWidth() - 1; + let yT = centerY - 13; + let yB = centerY + 13; + + for (let i = 0; i < steps.length; i++) { + xL += steps[i]; + xR -= steps[i]; + yT += stepsReversed[i]; + yB -= stepsReversed[i]; + + // Left Top + polyLeftTop.push(xL); + polyLeftTop.push(yT); + + // Left Bottom + polyLeftBottom.push(xL); + polyLeftBottom.push(yB); + + // Right Top + polyRightTop.push(xR); + polyRightTop.push(yT); + + // Right Bottom + polyRightBottom.push(xR); + polyRightBottom.push(yB); + } + + polyLeftTop.push(0, 88); + polyLeftBottom.push(0, 88); + polyRightTop.push(g.getWidth(), 88); + polyRightBottom.push(g.getWidth(), 88); + + g.fillPolyAA(polyLeftTop, true); + g.fillPolyAA(polyLeftBottom, true); + g.fillPolyAA(polyRightTop, true); + g.fillPolyAA(polyRightBottom, true); + }; + + let drawTime = function () { + g.clear(); + var d = new Date(); + var mins = d.getMinutes(); + + var offset = mins % 5; + var yTopLines = centerY - offset; + var topReached = false; + + var yBottomLines = centerY - offset + 5; + var bottomReached = false; + + drawCenterLine(); + + var lineEnd = lineEndDefault; + g.setFont("7x11Numeric7Seg", 2); + g.setFontAlign(0, 0); + + // gone + do { + switch (yTopLines - 88 + mins) { + case -60: + lineEnd = lineEndFull; + g.drawString(d.getHours() - 1, lineEnd + 10, yTopLines, true); + break; + case 0: + case 60: + lineEnd = lineEndFull; + g.drawString(d.getHours(), lineEnd + 10, yTopLines, true); + break; + case 45: + case -45: + case 15: + case -15: + case -75: + lineEnd = lineEndQuarter; + break; + case 30: + case -30: + lineEnd = lineEndHalf; + break; + default: + lineEnd = lineEndDefault; + } + g.drawLineAA(lineStart, yTopLines, lineEnd, yTopLines); + + yTopLines -= 5; + if (yTopLines < -4) { + topReached = true; + } + } while (!topReached); + + // upcoming + do { + switch (yBottomLines - 88 + mins) { + case 0: + case 60: + lineEnd = lineEndFull; + g.drawString(d.getHours() + 1, lineEnd + 10, yBottomLines, true); + break; + case 120: + lineEnd = lineEndFull; + g.drawString(d.getHours() + 2, lineEnd + 10, yBottomLines, true); + break; + case 15: + case 75: + case 135: + case 45: + case 105: + case 165: + lineEnd = lineEndQuarter; + break; + case 30: + case 90: + case 150: + lineEnd = lineEndHalf; + break; + default: + lineEnd = lineEndDefault; + } + g.drawLineAA(lineStart, yBottomLines, lineEnd, yBottomLines); + + yBottomLines += 5; + if (yBottomLines > 176) { + bottomReached = true; + } + } while (!bottomReached); + + queueDrawTime(); + }; + + + g.clear(); + drawTime(); + + Bangle.setUI( + { + mode: "clock", + remove: function () { + if (drawTimeout) clearTimeout(drawTimeout); + require("widget_utils").show(); + } + } + ); + + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); +} diff --git a/apps/measuretime/measuretime.png b/apps/measuretime/measuretime.png new file mode 100644 index 000000000..67425e1dc Binary files /dev/null and b/apps/measuretime/measuretime.png differ diff --git a/apps/measuretime/metadata.json b/apps/measuretime/metadata.json new file mode 100644 index 000000000..4c0db8b32 --- /dev/null +++ b/apps/measuretime/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "measuretime", + "name": "Measure Time", + "version": "0.1", + "description": "Measure Time in a fancy way.", + "icon": "small_measuretime.png", + "screenshots": [{ "url": "measuretime.png" }], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { "name": "measuretime.app.js", "url": "measuretime.app.js" }, + { "name": "measuretime.img", "url": "measuretime-icon.js", "evaluate": true } + ] +} diff --git a/apps/measuretime/small_measuretime.png b/apps/measuretime/small_measuretime.png new file mode 100644 index 000000000..74f476dad Binary files /dev/null and b/apps/measuretime/small_measuretime.png differ diff --git a/apps/measuretime/test.json b/apps/measuretime/test.json new file mode 100644 index 000000000..1b4123411 --- /dev/null +++ b/apps/measuretime/test.json @@ -0,0 +1,15 @@ +{ + "app" : "measuretime", + "tests" : [{ + "description": "Check memory usage after setUI", + "steps" : [ + {"t":"cmd", "js": "Bangle.loadWidgets()"}, + {"t":"cmd", "js": "eval(require('Storage').read('measuretime.app.js'))"}, + {"t":"cmd", "js": "Bangle.setUI()"}, + {"t":"saveMemoryUsage"}, + {"t":"cmd", "js": "eval(require('Storage').read('measuretime.app.js'))"}, + {"t":"cmd", "js":"Bangle.setUI()"}, + {"t":"checkMemoryUsage"} + ] + }] +} diff --git a/apps/meridian/ChangeLog b/apps/meridian/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/meridian/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/meridian/README.md b/apps/meridian/README.md new file mode 100644 index 000000000..1fdd03546 --- /dev/null +++ b/apps/meridian/README.md @@ -0,0 +1,11 @@ +# Meridian Clock + +An elegant clock with 2 clock info + +## Usage + +Tap on a widget and swipe left/right/up/down to change the displayed info + +## Creator + +Spioune diff --git a/apps/meridian/app-icon.js b/apps/meridian/app-icon.js new file mode 100644 index 000000000..8c37d4e1b --- /dev/null +++ b/apps/meridian/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgMAkEAwEEBIMQAo8IDpMYAq0wAoosCAoU8gNgAoV+gwRDvgXDsAFFEYgFR4AFQIgQFHgIFC8AFDg4HBhwWEngFE+AEDgYuEh4QEDgoASiGII4kMAYLWBgDKBggzEgb/YICSJBGwIFDghCDAoQ")) \ No newline at end of file diff --git a/apps/meridian/app.js b/apps/meridian/app.js new file mode 100644 index 000000000..26de3df5f --- /dev/null +++ b/apps/meridian/app.js @@ -0,0 +1,153 @@ +function getArcXY(centerX,centerY,radius,angle){ + var s,r = []; + s = 2 * Math.PI * angle / 360; + r.push(centerX + Math.round(Math.cos(s) * radius)); + r.push(centerY + Math.round(Math.sin(s) * radius)); + return r; +} + +function getArc(centerX,centerY,radius,startAngle,endAngle){ + var r = [], actAngle = startAngle; + var stepAngle = (radius + radius) * Math.PI / 60; + stepAngle = 6; + while(actAngle < endAngle){ + r = r.concat(getArcXY(centerX,centerY,radius,actAngle)); + actAngle += stepAngle; + actAngle = Math.min(actAngle,endAngle); + } + return r.concat(getArcXY(centerX,centerY,radius,endAngle)); +} + +function fillLine(x1,y1,x2,y2,thickness){ + const angle = Math.atan2(y2 - y1, x2 - x1); + const offset_x = thickness * Math.sin(angle) / 2; + const offset_y = thickness * Math.cos(angle) / 2; + g.fillPoly([ + x1 + offset_x, + y1 - offset_y, + x1 - offset_x, + y1 + offset_y, + x2 - offset_x, + y2 + offset_y, + x2 + offset_x, + y2 - offset_y + ],true); +} + +function drawInfoClock(itm,info,options){ + g.reset(); + + if (options.focus) + g.drawCircle(options.x+options.w/2, options.y+options.h/2, options.w/2+3); + + if (info.img) + g.drawImage(info.img, info.img.width ? options.x+options.w/2-info.img.width/2 : options.x, options.y); + + if(info.text) + g.setFont("6x8").setFontAlign(0,1).drawString(info.text, options.x+options.w/2,options.y+options.h); +} + +var clockInfoItems = require("clock_info").load(); + +clockInfoItems[0].items.unshift({ + name : "BatteryRing", + hasRange : true, + get : () => { + var s = 30; + var mid=s/2; + var v = E.getBattery(); + var g = Graphics.createArrayBuffer(s,s,4); + + const outerarc = getArc(mid,mid,14,-90,Math.max(v*3.6, 10)-90); + const innerarc = getArc(mid,mid,11,-92,Math.max(v*3.6, 10)-88); + + g.reset(); + g.transparent=0; + g.setColor('#00FF00').fillPoly([mid, mid].concat(outerarc)); + g.setColor('#000').fillPoly([mid, mid].concat(innerarc)); + g.setFont("6x8").setColor('#FFF').setFontAlign(0, 0).drawString(v, mid, mid); + return { v : v, min:0, max:100, img : g.asImage("object") }; + }, + show : function() { }, + hide : function() { }, +}); + +var topleft = require("clock_info").addInteractive(clockInfoItems, { + x : g.getWidth()*(1/4)-15, y: g.getHeight()*(1/4)-15, w: 30, h:30, + draw : (itm,info,options)=>{ + topleft.info = info; + topleft.options = options; + if(typeof draw === 'function') draw(); + } +}); + +var topright = require("clock_info").addInteractive(clockInfoItems, { + x : g.getWidth()*(3/4)-15, y: g.getHeight()*(1/4)-15, w: 30, h:30, + draw : (itm,info,options)=>{ + topright.info = info; + topright.options = options; + if(typeof draw === 'function') draw(); + } +}); + +var timeout; + +function draw(){ + if(timeout){ + clearTimeout(timeout); + timeout = undefined; + } + + g.setTheme({fg:0xFFFF, bg:0}); + g.reset().clear(); + + const mid=g.getWidth()/2; + + for(let i = 0; i<12;i++){ + const angle = i*Math.PI/6; + fillLine(mid, mid, mid+Math.cos(angle)*120, mid+Math.sin(angle)*120, 3); + } + + g.clearRect(10,10,g.getWidth()-10,g.getHeight()-10); + + if(topleft && topleft.info && topleft.options) + drawInfoClock(topleft.itm, topleft.info, topleft.options); + if(topright && topright.info && topright.options) + drawInfoClock(topright.itm, topright.info, topright.options); + + const now = new Date(); + + g.setFont("Vector",14); + g.setColor('#FFF'); + g.setFontAlign(0,0); + // Date (ex. MON 8) + g.drawString(require("locale").dow(now, 1).toUpperCase() + " " + now.getDate(), g.getWidth()/2, g.getHeight()*(3/4)); + + + let rhour = (now.getHours()*Math.PI/6)+(now.getMinutes()*Math.PI/30/12)-Math.PI/2; + let rmin = now.getMinutes()*Math.PI/30-Math.PI/2; + + // Middle circle + g.fillCircle(mid,mid,4); + + // Hour hand + fillLine(mid, mid, mid+Math.cos(rhour)*10, mid+Math.sin(rhour)*10,3); + fillLine(mid+Math.cos(rhour)*10, mid+Math.sin(rhour)*10, mid+Math.cos(rhour)*50, mid+Math.sin(rhour)*50,7); + + // Minute hand + fillLine(mid, mid, mid+Math.cos(rmin)*10, mid+Math.sin(rmin)*10,3); + fillLine(mid+Math.cos(rmin)*10, mid+Math.sin(rmin)*10, mid+Math.cos(rmin)*76, mid+Math.sin(rmin)*76,7); + + + if(new Date().getMinutes()==0){ + Bangle.buzz(); + } + + timeout = setTimeout(()=>{ + timeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +draw(); +Bangle.setUI("clock"); diff --git a/apps/meridian/icon.png b/apps/meridian/icon.png new file mode 100644 index 000000000..3edf38218 Binary files /dev/null and b/apps/meridian/icon.png differ diff --git a/apps/meridian/metadata.json b/apps/meridian/metadata.json new file mode 100644 index 000000000..ab4cdbed2 --- /dev/null +++ b/apps/meridian/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "meridian", + "name": "Meridian Clock", + "shortName": "Meridian", + "version": "0.01", + "description": "An elegant clock", + "screenshots": [{ "url": "screenshot.png" }], + "icon": "icon.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "meridian.app.js", "url": "app.js" }, + { "name": "meridian.img", "url": "app-icon.js", "evaluate": true } + ] +} diff --git a/apps/meridian/screenshot.png b/apps/meridian/screenshot.png new file mode 100644 index 000000000..2c86aca9b Binary files /dev/null and b/apps/meridian/screenshot.png differ diff --git a/apps/messagesoverlay/default.json b/apps/messagesoverlay/default.json new file mode 100644 index 000000000..e18b5b892 --- /dev/null +++ b/apps/messagesoverlay/default.json @@ -0,0 +1,6 @@ +{ + "autoclear": 30, + "border": 10, + "minfreemem": 2, + "systemTheme": true +} \ No newline at end of file diff --git a/apps/messagesoverlay/settings.js b/apps/messagesoverlay/settings.js new file mode 100644 index 000000000..cd76bf115 --- /dev/null +++ b/apps/messagesoverlay/settings.js @@ -0,0 +1,66 @@ +(function(back) { + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("messagesoverlay.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE="messagesoverlay.json"; + var settings; + readSettings(); + + function buildMainMenu(){ + var mainmenu = { + '' : { title: "Messages Overlay"}, + '< Back': back, + 'Border': { + value: settings.border, + min: 0, + max: Math.floor(g.getWidth()/2-50), + step: 1, + format: v=>v + "px", + onchange: v => { + writeSettings("border",v); + } + }, + 'Autoclear after': { + value: settings.autoclear, + min: 0, + max: 3600, + step: 10, + format: v=>v>0?v+"s":"Off", + onchange: v => { + writeSettings("autoclear",v); + } + }, + 'Theme': { + value: settings.systemTheme, + format: v=>v?"System":"low RAM", + onchange: v => { + writeSettings("systemTheme",v); + } + }, + 'Min. free RAM': { + value: settings.minfreemem, + min: 0, + max: process.memory().total/1000, + step: 1, + format: v=>v + "k free", + onchange: v => { + writeSettings("minfreemem",v); + } + } + }; + return mainmenu; + } + + E.showMenu(buildMainMenu()); +}); diff --git a/apps/messagesoverlay/test.json b/apps/messagesoverlay/test.json new file mode 100644 index 000000000..77e12caa6 --- /dev/null +++ b/apps/messagesoverlay/test.json @@ -0,0 +1,40 @@ +{ + "app" : "messagesoverlay", + "tests" : [{ + "description": "Test handler backgrounding", + "steps" : [ + {"t":"upload", "file": "modules/widget_utils.js", "as": "widget_utils"}, + {"t":"cmd", "js": "Bangle.loadWidgets()", "text": "Load widgets"}, + {"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "No swipe handlers"}, + {"t":"cmd", "js": "require('widget_utils').swipeOn(0)", "text": "Store widgets in overlay"}, + {"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler for widgets"}, + {"t":"emit", "event":"swipe", "paramsArray": [ 0, 1 ], "text": "Show widgets"}, + {"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler for widgets"}, + {"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Messenger',t:'add',type:'text',id:Date.now().toFixed(0),title:'title',body:'body'})", "text": "Show a message overlay"}, + {"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "No swipe handlers while message overlay is on screen"}, + {"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close message"}, + {"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler restored"} + ] + },{ + "description": "Test swipe handler backgrounding with fastloading (setUI)", + "steps" : [ + {"t":"cmd", "js": "Bangle.on('swipe',print)", "text": "Create listener for swipes"}, + {"t":"cmd", "js": "Bangle.setUI({mode: 'clock',remove: ()=>{}})", "text": "Init UI for clock"}, + {"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Messenger',t:'add',type:'text',id:Date.now().toFixed(0),title:'title',body:'body'})", "text": "Show a message overlay"}, + {"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "No swipe handlers while message overlay is on screen"}, + {"t":"cmd", "js": "Bangle.setUI()", "text": "Trigger removal of UI"}, + {"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "Still no swipe handlers"}, + {"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close message"}, + {"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler restored"} + ] + },{ + "description": "Test watch backgrounding", + "steps" : [ + {"t":"cmd", "js": "setWatch(print,BTN)", "text": "Create watch"}, + {"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Messenger',t:'add',type:'text',id:Date.now().toFixed(0),title:'title',body:'body'})", "text": "Show a message overlay"}, + {"t":"assertArray", "js": "global[\"\\xff\"].watches", "is":"undefinedOrEmpty", "text": "No watches while message overlay is on screen"}, + {"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close message"}, + {"t":"assert", "js": "global[\"\\xff\"].watches.length", "is":"equal", "to": "2", "text": "One watch restored, first entry is always empty"} + ] + }] +} diff --git a/apps/msgwakefup/ChangeLog b/apps/msgwakefup/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/msgwakefup/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/msgwakefup/README.md b/apps/msgwakefup/README.md new file mode 100644 index 000000000..43318b9f1 --- /dev/null +++ b/apps/msgwakefup/README.md @@ -0,0 +1,19 @@ +# Message Wake On Face Up + +Temporarily activate wake on face up function when a new message is auto displayed. + +## Usage + +This is a bootloader app and only needs to be installed to add the functionality to the watch. + +## Notes + +Tried with "Message UI" app - with and without "Fastload Utils" installed. + +## Requests + +Mention @thyttan in an issue on the espruino/BangleApps repository. + +## Creator + +thyttan and Gordon Williams diff --git a/apps/msgwakefup/app.png b/apps/msgwakefup/app.png new file mode 100644 index 000000000..83d7e9add Binary files /dev/null and b/apps/msgwakefup/app.png differ diff --git a/apps/msgwakefup/boot.js b/apps/msgwakefup/boot.js new file mode 100644 index 000000000..f30de7a1b --- /dev/null +++ b/apps/msgwakefup/boot.js @@ -0,0 +1,9 @@ +// If doing regular loads, not Bangle.load, this is used: +if (global.__FILE__=="messagegui.new.js") Bangle.setOptions({wakeOnFaceUp:true}); + +// If Fastload Utils is installed this is used: +Bangle.on("message", (_, msg)=>{if (Bangle.CLOCK && msg.new) { + setTimeout(()=>{ + if (global.__FILE__=="messagegui.new.js") Bangle.setOptions({wakeOnFaceUp:true}); + },700) // It feels like there's a more elegant solution than checking the filename after 700 milliseconds. But this at least seems to work w/o sometimes activating when it shouldn't. +}}); diff --git a/apps/msgwakefup/metadata.json b/apps/msgwakefup/metadata.json new file mode 100644 index 000000000..7f97b3221 --- /dev/null +++ b/apps/msgwakefup/metadata.json @@ -0,0 +1,13 @@ +{ "id": "msgwakefup", + "name": "Message wake on face up", + "version":"0.01", + "description": "Temporarily activate wake on face up function when a new message is auto displayed.", + "icon": "app.png", + "tags": "messages,tweak", + "type": "bootloader", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"msgwakefup.boot.js","url":"boot.js"} + ] +} diff --git a/apps/nostt/ChangeLog b/apps/nostt/ChangeLog new file mode 100644 index 000000000..5314d96b9 --- /dev/null +++ b/apps/nostt/ChangeLog @@ -0,0 +1 @@ +1.00: NOS Teletekst finished! \ No newline at end of file diff --git a/apps/nostt/README.md b/apps/nostt/README.md new file mode 100644 index 000000000..f9b57cc66 --- /dev/null +++ b/apps/nostt/README.md @@ -0,0 +1,12 @@ +# NOS Teletekst + + +Dutch Teletekst using the NOS Teletekst api. Requires http access via BangleJS GadgetBridge. See https://www.espruino.com/Gadgetbridge. Make sure `Allow Internet Access` is enabled. + +## Usage + +Tap once to bring up a numpad to enter the desired page. You can also swipe left/right to change the page, or swipe up/down to walk through the subpages. + +## Creator + +[Albert van der Meer](https://github.com/avandermeer) \ No newline at end of file diff --git a/apps/nostt/metadata.json b/apps/nostt/metadata.json new file mode 100644 index 000000000..b4f2cc068 --- /dev/null +++ b/apps/nostt/metadata.json @@ -0,0 +1,17 @@ +{ + "id":"nostt", + "name":"NOS Teletekst", + "shortName": "Teletekst", + "version": "1.00", + "description": "Dutch Teletekst using the NOS Teletekst api. Requires http access via BangleJS GadgetBridge.", + "type": "app", + "storage": [ + {"name":"nostt.app.js","url":"nostt.app.js"}, + {"name":"nostt.img","url":"nostt.icon.js","evaluate":true} + ], + "readme": "README.md", + "icon":"nostt_logo.png", + "supports": ["BANGLEJS2"], + "screenshots": [{"url": "nostt_screenshot_1.png"}, {"url": "nostt_screenshot_2.png"}], + "tags": "nos,teletext,teletekst,news,weather" +} \ No newline at end of file diff --git a/apps/nostt/nostt.app.js b/apps/nostt/nostt.app.js new file mode 100644 index 000000000..5eefa928f --- /dev/null +++ b/apps/nostt/nostt.app.js @@ -0,0 +1,505 @@ +class View { + + constructor() { + + this.navigationState = { + prevPage: { + p: undefined, + s: undefined, + }, + prevSubPage: { + p: undefined, + s: undefined, + }, + nextPage: { + p: undefined, + s: undefined, + }, + nextSubPage: { + p: undefined, + s: undefined, + }, + currentPage: { + p: undefined, + s: undefined, + }, + }; + + this.colorArray = { + 0: [0, 0, 0], + 1: [1, 0, 0], + 2: [0, 1, 0], + 3: [1, 1, 0], + 4: [0, 0, 1], + 5: [1, 0, 1], + 6: [0, 1, 1], + 7: [1, 1, 1], + 16: [0, 0, 0], + 17: [1, 0, 0], + 18: [0, 1, 0], + 19: [1, 1, 0], + 20: [0, 0, 1], + 21: [1, 0, 1], + 22: [0, 1, 1], + 23: [1, 1, 1], + }; + + + } + + start() { + g.clear(); + if (this.nextStartPage) { + this.show(this.nextStartPage); + this.nextStartPage = undefined; + } + else { + if (this.navigationState.currentPage.p) { + this.show(this.navigationState.currentPage.p); + } + else { + this.show(101); //load default + } + } + + } + + split_at_fourty(res, value) { + res.push(value.substring(0, 40)); + if (value.length > 40) { // at least two rows + return this.split_at_fourty(res, value.substring(40)); + } + else { + return res; + } + + } + +// strToUtf8Bytes(str) { +// const utf8 = []; +// for (let ii = 0; ii < str.length; ii++) { +// let charCode = str.charCodeAt(ii); +// if (charCode < 0x80) utf8.push(charCode); +// else if (charCode < 0x800) { +// utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f)); +// } else if (charCode < 0xd800 || charCode >= 0xe000) { +// utf8.push(0xe0 | (charCode >> 12), 0x80 | ((charCode >> 6) & 0x3f), 0x80 | (charCode & 0x3f)); +// } else { +// ii++; +// // Surrogate pair: +// // UTF-16 encodes 0x10000-0x10FFFF by subtracting 0x10000 and +// // splitting the 20 bits of 0x0-0xFFFFF into two halves +// charCode = 0x10000 + (((charCode & 0x3ff) << 10) | (str.charCodeAt(ii) & 0x3ff)); +// utf8.push( +// 0xf0 | (charCode >> 18), +// 0x80 | ((charCode >> 12) & 0x3f), +// 0x80 | ((charCode >> 6) & 0x3f), +// 0x80 | (charCode & 0x3f), +// ); +// } +// } +// return utf8; +// } + + loadPrevPage() { + if (this.navigationState.prevPage.p) { + this.show(this.navigationState.prevPage.p, this.navigationState.prevPage.s); + } + } + + loadNextPage() { + if (this.navigationState.nextPage.p) { + this.show(this.navigationState.nextPage.p, this.navigationState.nextPage.s); + } + } + + loadPrevSubPage() { + if (this.navigationState.prevSubPage.p) { + this.show(this.navigationState.prevSubPage.p, this.navigationState.prevSubPage.s); + } + } + + loadNextSubPage() { + if (this.navigationState.nextSubPage.p) { + this.show(this.navigationState.nextSubPage.p, this.navigationState.nextSubPage.s); + } + } + + handleSwipe(lr, ud){ + if (lr == -1 && ud == 0) { + this.loadNextPage(); + } + if (lr == 1 && ud == 0) { + this.loadPrevPage(); + } + if (lr == 0 && ud == 1) { + this.loadPrevSubPage(); + } + if (lr == 0 && ud == -1) { + this.loadNextSubPage(); + } + } + + show(pageId, subPageId) { + if(!subPageId){ + subPageId = 1; + } + + if (Bangle.http) { + Bangle.http('https://teletekst-data.nos.nl/page/' + pageId + '-' + subPageId).then((data) => { + + const res = data.resp; + g.clear(); + + + this.navigationState = { + prevPage: { + p: undefined, + s: undefined, + }, + prevSubPage: { + p: undefined, + s: undefined, + }, + nextPage: { + p: undefined, + s: undefined, + }, + nextSubPage: { + p: undefined, + s: undefined, + }, + currentPage: { + p: pageId, + s: subPageId, + }, + }; + + // set next -, previous -, next sub - and previous sub page + let navNIndex = res.indexOf('pn=n_'); + if (navNIndex > -1) { + this.navigationState.nextPage.p = parseInt(res.substring(navNIndex + 5, navNIndex + 8)); + this.navigationState.nextPage.s = parseInt(res.substring(navNIndex + 9, navNIndex + 10)); + } + let navPIndex = res.indexOf('pn=p_'); + if (navPIndex > -1) { + this.navigationState.prevPage.p = parseInt(res.substring(navPIndex + 5, navPIndex + 8)); + this.navigationState.prevPage.s = parseInt(res.substring(navPIndex + 9, navPIndex + 10)); + } + let navPSIndex = res.indexOf('pn=ps'); + if (navPSIndex > -1) { + this.navigationState.prevSubPage.p = parseInt(res.substring(navPSIndex + 5, navPSIndex + 8)); + this.navigationState.prevSubPage.s = parseInt(res.substring(navPSIndex + 9, navPSIndex + 10)); + } + let navNSIndex = res.indexOf('pn=ns'); + if (navNSIndex > -1) { + this.navigationState.nextSubPage.p = parseInt(res.substring(navNSIndex + 5, navNSIndex + 8)); + this.navigationState.nextSubPage.s = parseInt(res.substring(navNSIndex + 9, navNSIndex + 10)); + } + + let split = E.toString(res.split('
')[1].split('
')[0]); + + this.render(split); + }); + } + } + + + + + render(source) { + + g.setFontAlign(-1, -1); + g.setFont('4x6'); + + + const bytes = E.toUint8Array(E.decodeUTF8(source)); + let rowIndex = 0; + let totalIndex = 0; + let charIndex = 0; + + for (let charByte of bytes) { + { + if ((charByte >= 0 && charByte <= 7) || (charByte >= 16 && charByte <= 23)) { + const color = this.colorArray[charByte]; + g.setColor(color[0], color[1], color[2]); + } + } + g.drawString(source[totalIndex], (charIndex * 4) + 6, rowIndex * 7); + charIndex++; + totalIndex++; + if (charIndex == 40) { + rowIndex++; + charIndex = 0; + g.flip(); + } + } + } + + +} + +const BUTTON_BORDER_WITH = 2; + +class Button { +// position; +// value; +// highlightTimeoutId; + + + constructor(position, value) { + this.position = position; + this.value = value; + } + + draw(highlight) { + g.setColor(g.theme.fg); + g.fillRect( + this.position.x1, + this.position.y1, + this.position.x2, + this.position.y2 + ); + + if (highlight) { + g.setColor(g.theme.bgH); + } else { + g.setColor(g.theme.bg); + } + g.fillRect( + this.position.x1 + BUTTON_BORDER_WITH, + this.position.y1 + BUTTON_BORDER_WITH, + this.position.x2 - BUTTON_BORDER_WITH, + this.position.y2 - BUTTON_BORDER_WITH + ); + + g.setColor(g.theme.fg); + g.setFontAlign(0, 0); + g.setFont("Vector", 35); + g.drawString( + this.value, + this.position.x1 + (this.position.x2 - this.position.x1) / 2 + 2, + this.position.y1 + (this.position.y2 - this.position.y1) / 2 + 2 + ); + } + + handleTouchInput(n, e) { + if ( + e.x >= this.position.x1 && + e.x <= this.position.x2 && + e.y >= this.position.y1 && + e.y <= this.position.y2 + ) { + this.draw(true); // draw to highlight + this.highlightTimeoutId = setTimeout(() => { + this.draw(); + this.highlightTimeoutId = undefined; + }, 100); + return this.value; + } + else { + return undefined; + } + } + + disable() { + // disable button + if (this.highlightTimeoutId) { + clearTimeout(this.highlightTimeoutId); + this.highlightTimeoutId = undefined; + } + } + +} + +class Input { + + constructor(callback) { + this.inputCallback = callback; + this.inputVal = ""; + + let button1 = new Button({ x1: 1, y1: 35, x2: 58, y2: 70 }, '1'); + let button2 = new Button({ x1: 60, y1: 35, x2: 116, y2: 70 }, '2'); + let button3 = new Button({ x1: 118, y1: 35, x2: 174, y2: 70 }, '3'); + + let button4 = new Button({ x1: 1, y1: 72, x2: 58, y2: 105 }, '4'); + let button5 = new Button({ x1: 60, y1: 72, x2: 116, y2: 105 }, '5'); + let button6 = new Button({ x1: 118, y1: 72, x2: 174, y2: 105 }, '6'); + + let button7 = new Button({ x1: 1, y1: 107, x2: 58, y2: 140 }, '7'); + let button8 = new Button({ x1: 60, y1: 107, x2: 116, y2: 140 }, '8'); + let button9 = new Button({ x1: 118, y1: 107, x2: 174, y2: 140 }, '9'); + + let buttonOK = new Button({ x1: 1, y1: 142, x2: 58, y2: 174 }, "OK"); + let button0 = new Button({ x1: 60, y1: 142, x2: 116, y2: 174 }, "0"); + let buttonDelete = new Button({ x1: 118, y1: 142, x2: 174, y2: 174 }, "<-"); + + this.inputButtons = [ + button1, + button2, + button3, + button4, + button5, + button6, + button7, + button8, + button9, + buttonOK, + button0, + buttonDelete, + ]; + } + + handleTouchInput(n, e) { + let res = 'none'; + for (let button of this.inputButtons) { + const touchResult = button.handleTouchInput(n, e); + if (touchResult) { + res = touchResult; + } + } + + switch (res) { + case 'OK': + if(this.inputVal.length == 3){ + this.inputCallback(parseInt(this.inputVal)); + } + break; + case '<-': + this.removeNumber(); + this.drawField(); + break; + case 'none': + break; + default: + this.appendNumber(parseInt(res)); + this.drawField(); + } + + } + + + hide() { + for (let button of this.inputButtons) { + button.disable(); + } + } + + start(preset) { + if (preset) { + this.inputVal = preset.toString(); + } + else { + this.inputVal = ''; + } + + this.draw(); + } + + appendNumber(number) { + if (number === 0 && this.inputVal.length === 0) { + return; + } + + if (this.inputVal.length <= 2) { + this.inputVal = this.inputVal + number; + } + } + + removeNumber() { + if (this.inputVal.length > 0) { + this.inputVal = this.inputVal.slice(0, -1); + } + } + + reset() { + this.inputVal = ""; + } + + draw() { + g.clear(); + this.drawButtons(); + this.drawField(); + } + + drawButtons() { + for (let button of this.inputButtons) { + button.draw(); + } + } + + drawField() { + g.clearRect(0, 0, 176, 34); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + g.setFont("Vector:26x40"); + g.drawString(this.inputVal, 2, 0); + } +} + +// require('./Input'); + +class NOSTeletekstApp { + + constructor() { + console.log("this is the teletekst app!"); + this.isLeaving = false; + this.viewMode= 'VIEW'; + this.view = new View(); + this.input = new Input((newVal)=>this.inputHandler(newVal)); + this.view.start(); + + Bangle.setUI({ + mode: "custom", + remove: () => { + this.isLeaving = true; + console.log("teletext app: i am packing my stuff, goodbye"); + require("widget_utils").show(); // re-show widgets + }, + touch: (n, e) => { + if (this.viewMode == 'VIEW') { + // we need to go to input mode + this.setViewMode('INPUT'); + return; + } + if (this.viewMode == 'INPUT') { + this.input.handleTouchInput(n, e); + return; + + } + }, + swipe: (lr, ud) => { + if (this.viewMode == 'VIEW') { + this.view.handleSwipe(lr,ud); + } + if (this.viewMode == 'INPUT') { + if(lr == 1 && ud == 0){ + this.setViewMode('VIEW'); + } + } + } + + }); + + } + + inputHandler(input){ + // set viewMode back to view + this.view.nextStartPage = input; + this.setViewMode('VIEW'); + } + + setViewMode(newViewMode){ + this.viewMode = newViewMode; + if(newViewMode=='INPUT'){ + this.input.start(); + } + if(newViewMode=='VIEW'){ + this.input.hide(); + this.view.start(); + } + } + + +} +new NOSTeletekstApp(); \ No newline at end of file diff --git a/apps/nostt/nostt.icon.js b/apps/nostt/nostt.icon.js new file mode 100644 index 000000000..b9ef929be --- /dev/null +++ b/apps/nostt/nostt.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AH4A/AH4AWgmSAwn/wACB/0Agf/4EAhMkyVIB4MB/4AB/ARGpIRBpMggEPCIQAC4ARCgQRDkkAv4jEAQIRLAQIRC/4RCiVJIgJKBwARCgPACIxWCCIn8NwQAB8YRFgIRDNARcFPQgRBNYZHB+IRDEYyPDAAPwn4RJIggRBg4RDNYrGCJQQRGkCSCWYP4CIgpCUI7FFCIMfa5DFFCIL7GJQQRLgARBoBXCO4KhBAH4A/AH4A/AD4A=")) \ No newline at end of file diff --git a/apps/nostt/nostt_logo.png b/apps/nostt/nostt_logo.png new file mode 100644 index 000000000..bf8f0d47d Binary files /dev/null and b/apps/nostt/nostt_logo.png differ diff --git a/apps/nostt/nostt_screenshot_1.png b/apps/nostt/nostt_screenshot_1.png new file mode 100644 index 000000000..ad65ecba7 Binary files /dev/null and b/apps/nostt/nostt_screenshot_1.png differ diff --git a/apps/nostt/nostt_screenshot_2.png b/apps/nostt/nostt_screenshot_2.png new file mode 100644 index 000000000..0faa9b1f6 Binary files /dev/null and b/apps/nostt/nostt_screenshot_2.png differ diff --git a/apps/phystrax/ChangeLog b/apps/phystrax/ChangeLog new file mode 100644 index 000000000..3bc4ef732 --- /dev/null +++ b/apps/phystrax/ChangeLog @@ -0,0 +1 @@ +0.01: New App. \ No newline at end of file diff --git a/apps/phystrax/README.md b/apps/phystrax/README.md new file mode 100644 index 000000000..dbb1272ef --- /dev/null +++ b/apps/phystrax/README.md @@ -0,0 +1,8 @@ +# Capstone Wearable +Research has shown that active learning approaches in classrooms, which emphasize the construction of knowledge and reflection on the learning process, significantly enhance the student learning experience and lead to better success in the classroom. The Active Learning Initiative at the University of Georgia focuses on instructor development, student engagement, and classroom enhancement. + +This project focuses specifically on classroom enhancement to create a device that will enhance the learning experience of students in physiology classes by allowing them to learn about and analyze their physiological patterns. The project must adhere to the constraints of a one-hundred dollar per watch budget, the ability to be scaled to a classroom of three-hundred twenty students, and provide data with at least 80% accuracy. By giving students access to their heart rate (HR) and heart rate variability (HRV) data, the goal is that students can better understand the function of the heart as well as the physiological conditions that can be affected by the heart. The original concept was to develop a wrist-wearable, screen-less device featuring sensors that detect a user’s heart rate by emitting LEDs that detect the expansion of blood vessels in the wrist and the quantity of light transmitted in the vascular bed to obtain heart rate and blood oxygen data and the heart rate variability calculated using an algorithm that takes the root-mean-square of interbeat intervals between successive heartbeats. + +After prototyping this custom device and testing its efficacy, the accuracy of the data produced by the sensor was not within the accuracy criteria, and the decision was made to pivot to an existing on-market device coupled with custom software to meet the needs of the client. The analysis software created for the device allows the students to analyze their physiological data and permits for their instructor to access their data anonymously. The data, coupled with survey data entered by the students daily, provides insight into the effect of variables such as sleep, exercise, and caffeine intake to analyze the effects that these have on their physiological measurements. To evaluate the device's efficacy in achieving the goal of 80% accuracy, the wrist wearable device was tested on several different skin tones doing various activities and measuring data over varying intervals and then compared against the data of Apple Watches. + +This project resulted in the creation of a custom data analysis software for the use of students on a Bangle.js smartwatch device that can be continually developed in the future to reach the goal of an inexpensive, easy-to-use device for students to use in active learning classrooms to enhance their understanding of physiology by analyzing their own physiological data in class. \ No newline at end of file diff --git a/apps/phystrax/app-icon.js b/apps/phystrax/app-icon.js new file mode 100644 index 000000000..a535bf5e3 --- /dev/null +++ b/apps/phystrax/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AH4ABgVJkmAA4cEyVJCIwIBkmSAwUBAoIIBCAgaCBYNIA4IFCFgwIDF4Q7CBAWQFg4CBkESCIg+DhIRFAQ9ACKYOLMRWf5IRQ/IRP/4RlzwRKyf5k4RC/xcFCISPBKwMn+YRB/4RFUImf/4RCEwIRIa4P/AAPz/YRDHwLXEgP//1P/+T/8vJQIZCCIkAn/yCIOSv8/2YQCCIOQCIY+CCIYmB8g1CCIkECIM8CII4CLIeACIcAMQd//mSvYRDCAkAiQRFWYcgCIsCCJIQFbQl/8jCGAAq2ByVPCIiwCAAq2BCILCECA6SEAQaMEAAqSCRhIAFCIoQKSQiMHQBJ6IQBB6IQBAQNNwRoLAH4AoA=")) \ No newline at end of file diff --git a/apps/phystrax/app.js b/apps/phystrax/app.js new file mode 100644 index 000000000..3ecb5d04e --- /dev/null +++ b/apps/phystrax/app.js @@ -0,0 +1,157 @@ +let isMeasuring = false; +let currentHR = null; +let lcdTimeout; +let logData = []; +let bpmValues = []; + +function startMeasure() { + logData = []; + isMeasuring = true; + Bangle.setLCDTimeout(0); + lcdTimeout = setTimeout(() => { + Bangle.setLCDTimeout(50); + }, 50000); + + setTimeout(() => { + Bangle.setHRMPower(1); // starts HRM + Bangle.on('HRM', handleHeartRate); + Bangle.buzz(200, 10); // Buzz to indicate measurement start + drawScreen(); + }, 500); +} + +function stopMeasure() { + isMeasuring = false; + clearTimeout(lcdTimeout); + Bangle.setLCDTimeout(10); + Bangle.setHRMPower(0); + Bangle.removeAllListeners('HRM'); //stop HRM + saveDataToCSV(); // Save data to CSV when measurement stops + Bangle.buzz(200, 10); // Buzz to indicate measurement stop + drawScreen(); +} + +function handleHeartRate(hrm) { + if (isMeasuring && hrm.confidence > 90) { + let date = new Date(); + let dateStr = require("locale").date(date); + let timeStr = require("locale").time(date, 1); + let seconds = date.getSeconds(); + let timestamp = `${dateStr} ${timeStr}:${seconds}`; // Concatenate date, time, and seconds + currentHR = hrm.bpm; + + logData.push({ timestamp: timestamp, heartRate: currentHR }); + bpmValues.push(currentHR); // Store heart rate for HRV calculation + if (bpmValues.length > 30) bpmValues.shift(); // Keep last 30 heart rate values + // Calculate and add SDNN (standard deviation of NN intervals) to the last log entry + logData[logData.length - 1].hrv = calcSDNN(); + drawScreen(); + + } +} + +function calcSDNN() { + if (bpmValues.length < 5) return 0; // No calculation if insufficient data + + // Calculate differences between adjacent heart rate values + const differences = []; + for (let i = 1; i < bpmValues.length; i++) { + differences.push(Math.abs(bpmValues[i] - bpmValues[i - 1])); + } + + // Calculate mean difference + const meanDifference = differences.reduce((acc, val) => acc + val, 0) / differences.length; + + // Calculate squared differences from mean difference + const squaredDifferences = differences.map(diff => Math.pow(diff - meanDifference, 2)); + + // Calculate mean squared difference + const meanSquaredDifference = squaredDifferences.reduce((acc, val) => acc + val, 0) / squaredDifferences.length; + + // Calculate SDNN (standard deviation of NN intervals) + const sdnn = Math.sqrt(meanSquaredDifference); + + return sdnn; +} + +function drawScreen(message) { + g.clear(); // Clear the display + + // Set the background color + g.setColor('#95E7FF'); + + // Fill the entire display with the background color + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + // Set font and alignment for drawing text + g.setFontAlign(0, 0); + g.setFont('Vector', 15); + + // Draw the title + g.setColor('#000000'); // Set text color to black + g.drawString('Heart Rate Monitor', g.getWidth() / 2, 10); + + if (isMeasuring) { + // Draw measuring status + g.setFont('6x8', 2); + g.drawString('Measuring...', g.getWidth() / 2, g.getHeight() / 2 - 10); + + // Draw current heart rate if available + g.setFont('6x8', 4); + if (currentHR !== null) { + g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 20); + g.setFont('6x8', 1.6); + g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 20); + } + + // Draw instructions + g.setFont('6x8', 1.5); + g.drawString('Press button to stop', g.getWidth() / 2, g.getHeight() / 2 + 42); + } else { + // Draw last heart rate + if (currentHR !== null && currentHR > 0) { + g.setFont('Vector', 12); + g.drawString('Last Heart Rate:', g.getWidth() / 2, g.getHeight() / 2 - 20); + g.setFont('6x8', 4); + g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 10); + g.setFont('6x8', 1.6); + g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 12); + } else { + g.setFont('6x8', 2); + g.drawString('No data', g.getWidth() / 2, g.getHeight() / 2 + 5); + g.setFont('6x8', 1); + g.drawString(message || 'Press button to start', g.getWidth() / 2, g.getHeight() / 2 + 30); + } + } + + // Update the display + g.flip(); +} + +function saveDataToCSV() { + let fileName = "phystrax_hrm.csv"; + let file = require("Storage").open(fileName, "a"); // Open the file for appending + + // Check if the file is empty (i.e., newly created) + if (file.getLength() === 0) { + // Write the header if the file is empty + file.write("Timestamp,Heart Rate(bpm),HRV(ms)\n"); + } + + // Append the data + logData.forEach(entry => { + let scaledHRV = entry.hrv * 13.61; + file.write(`${entry.timestamp},${entry.heartRate},${scaledHRV}\n`); + }); + +} + +setWatch(function() { + if (!isMeasuring) { + startMeasure(); + } else { + stopMeasure(); + } +}, BTN1, { repeat: true, edge: 'rising' }); + +drawScreen(); diff --git a/apps/phystrax/app.png b/apps/phystrax/app.png new file mode 100644 index 000000000..b9c10ca46 Binary files /dev/null and b/apps/phystrax/app.png differ diff --git a/apps/phystrax/interface.html b/apps/phystrax/interface.html new file mode 100644 index 000000000..4d1933df6 --- /dev/null +++ b/apps/phystrax/interface.html @@ -0,0 +1,65 @@ + + + + + +
+ + + + + + + diff --git a/apps/phystrax/metadata.json b/apps/phystrax/metadata.json new file mode 100644 index 000000000..bcc0be70d --- /dev/null +++ b/apps/phystrax/metadata.json @@ -0,0 +1,16 @@ +{ "id": "phystrax", + "name": "PhysTrax", + "shortName":"PhysTrax", + "icon": "app.png", + "version":"0.01", + "description": "Tracking physiological measurements to support active learning in classrooms", + "tags": "health", + "interface": "interface.html", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"phystrax.app.js","url":"app.js"}, + {"name":"phystrax.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"phystrax_hrm.csv"}] +} diff --git a/apps/promenu/bootb2.ts b/apps/promenu/bootb2.ts new file mode 100644 index 000000000..9342d2ec9 --- /dev/null +++ b/apps/promenu/bootb2.ts @@ -0,0 +1,196 @@ +type ActualMenuItem = Exclude; + +E.showMenu = (items?: Menu): MenuInstance => { + const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => { + const pp = []; + pp.push(...g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r])); + pp.push(...g.quadraticBezier([x2, y2 - r, x2, y2, x2 - r, y2])); + pp.push(...g.quadraticBezier([x1 + r, y2, x1, y2, x1, y2 - r])); + pp.push(...g.quadraticBezier([x1, y1 + r, x1, y1, x1 + r, y1])); + return pp; + }; + const fillRectRnd = (x1: number, y1: number, x2: number, y2: number, r: number, c: ColorResolvable) => { + g.setColor(c); + g.fillPoly(RectRnd(x1, y1, x2, y2, r)); + g.setColor(255, 255, 255); + }; + let options = items && items[""] || {}; + if (items) delete items[""]; + const menuItems = Object.keys(items); + + const fontHeight = options.fontHeight||25; + + let selected = options.scroll || options.selected || 0; + + const ar = Bangle.appRect; + g.reset().clearRect(ar); + + const x = ar.x; + const x2 = ar.x2; + let y = ar.y; + const y2 = ar.y2 - 12; // padding at end for arrow + if (options.title) + y += 22; + + let lastIdx = 0; + let selectEdit: undefined | ActualMenuItem = undefined; + + const l = { + draw: (rowmin?: number, rowmax?: number) => { + let rows = 0|Math.min((y2 - y) / fontHeight, menuItems.length); + let idx = E.clip(selected - (rows>>1), 0, menuItems.length - rows); + + if (idx != lastIdx) rowmin=undefined; // redraw all if we scrolled + lastIdx = idx; + let iy = y; + g.reset().setFontAlign(0, -1, 0).setFont12x20(); + if (options.predraw) options.predraw(g); + if (rowmin === undefined && options.title) + g.drawString(options.title, (x + x2) / 2, y - 21).drawLine(x, y - 2, x2, y - 2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 4; + if (rowmin !== undefined) { + if (idx < rowmin) { + iy += fontHeight * (rowmin - idx); + idx = rowmin; + } + if (rowmax && idx + rows > rowmax) { + rows = 1 + rowmax - rowmin; + } + } + while (rows--) { + const name = menuItems[idx]; + const item = items![name]! as ActualMenuItem; + + const hl = (idx === selected && !selectEdit); + if(g.theme.dark){ + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg + 40); + }else{ + fillRectRnd(x, iy, x2, iy + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg - 20); + } + + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign( - 1, -1); + + let v; + if (typeof item === "object") { + v = "format" in item + ? (item.format as any)(item.value) // format(), value: T + : item.value; + if (typeof v !== "string") v = `${v}`; + } else { + v = ""; + } + + /*???*/{ + if(name.length >= 17 - v.length && typeof item === "object"){ + g.drawString(name.substring(0, 12 - v.length) + "...", x + 3.7, iy + 2.7); + }else if(name.length >= 15){ + g.drawString(name.substring(0, 15) + "...", x + 3.7, iy + 2.7); + }else{ + g.drawString(name, x + 3.7, iy + 2.7); + } + + let xo = x2; + if (selectEdit && idx === selected) { + xo -= 24 + 1; + g.setColor(g.theme.fgH) + .drawImage( + "\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@", + xo, + iy + (fontHeight - 10) / 2, + {scale:2}, + ); + } + g.setFontAlign(1, -1); + g.drawString(v, xo - 2, iy + 1); + } + + g.setColor(g.theme.fg); + iy += fontHeight; + idx++; + } + g.setFontAlign( - 1, -1); + g.setColor((idx < menuItems.length)?g.theme.fg:g.theme.bg).fillPoly([72, 166, 104, 166, 88, 174]); + g.flip(); + }, + select: () => { + const item = items![menuItems[selected]] as ActualMenuItem; + + if (typeof item === "function") { + item(); + } else if (typeof item === "object") { + if (typeof item.value === "number") { + selectEdit = selectEdit ? undefined : item; + } else { + if (typeof item.value === "boolean") + item.value = !item.value; + + if (item.onchange) + item.onchange(item.value as boolean); + } + l.draw(); + } + }, + move: (dir: number) => { + const item = selectEdit; + + if (typeof item === "object" && typeof item.value === "number") { + const orig = item.value; + + item.value += (-dir||1) * (item.step||1); + + if ("min" in item && item.value < item.min) + item.value = item.wrap ? item.max as number : item.min; + + if ("max" in item && item.value > item.max) + item.value = item.wrap ? item.min as number : item.max; + + if (item.value !== orig) { + if (item.onchange) + item.onchange(item.value); + + l.draw(selected, selected); + } + + } else { + const lastSelected = selected; + selected = (selected + dir + /*keep +ve*/menuItems.length) % menuItems.length; + l.draw(Math.min(lastSelected, selected), Math.max(lastSelected, selected)); + } + }, + }; + + l.draw(); + + let back = options.back; + if (!back) { + const backItem = items && items["< Back"]; + if (typeof backItem === "function") + back = backItem; + else if (backItem && "back" in backItem) + back = backItem.back; + } + let onSwipe: SwipeCallback | undefined; + if (typeof back === "function") { + const back_ = back; + onSwipe = (lr/*, ud*/) => { + if (lr < 0) back_(); + }; + Bangle.on('swipe', onSwipe); + } + + Bangle.setUI({ + mode: "updown", + back, + remove: () => { + Bangle.removeListener("swipe", onSwipe); + }, + } as SetUIArg<"updown">, + dir => { + if (dir) l.move(dir); + else l.select(); + }); + + return l; +}; diff --git a/apps/rest/ChangeLog b/apps/rest/ChangeLog new file mode 100644 index 000000000..5453557bc --- /dev/null +++ b/apps/rest/ChangeLog @@ -0,0 +1 @@ +0.01: First Release diff --git a/apps/rest/README.md b/apps/rest/README.md new file mode 100644 index 000000000..59f031cc3 --- /dev/null +++ b/apps/rest/README.md @@ -0,0 +1,16 @@ +# Rest - Workout Timer + +An app to keep track of time when not lifting things and keep track of your sets when lifting things. + +![screenshot](screenshot1.png) + +## Usage + +Install the app. Set the number of sets and the rest between sets. Once you tap "GO" the app is only +operated using the physical button on the watch, to avoid accidental touches during workout. + +The watch will vibrate to let you know when your rest time is up. + +## Credits + +Created by: devsnd diff --git a/apps/rest/app-icon.js b/apps/rest/app-icon.js new file mode 100644 index 000000000..fcc93857f --- /dev/null +++ b/apps/rest/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkA///+czAAk/BIILM+eIAAwMCme7AAwLCCw4ABEQIWHAAIuJGAX7C5M//AXJx87C5O/nAXJwYXK2YXax6UGC4e/UIYXJ/42DC6B7BwYwDC4iTGI44vJYgpHSC5JEBI5LzGL7gXjU64XKAA4XDAA4XYIYIAIx4XKV4IXJn6LGAAc//4XJOAgAGPoQuIBYMzFxIYCmYAEBQYLMABQWGDAgLLm93AA1zKYQAIEQIWHAAM/FxAwCFxAABl4XWuYXzUIQXHRAX/+QXGYoYXIEgMzmQXHco5HEn8nI6YXMJAQXUJQwXPCgQXsO8szd5IAGC4oAFC/4AHl5xEAAv/+YXJRQIwISoUyCw8jXQQALHRH/")) diff --git a/apps/rest/app.png b/apps/rest/app.png new file mode 100644 index 000000000..c04ed7831 Binary files /dev/null and b/apps/rest/app.png differ diff --git a/apps/rest/metadata.json b/apps/rest/metadata.json new file mode 100644 index 000000000..bdce75cd6 --- /dev/null +++ b/apps/rest/metadata.json @@ -0,0 +1,15 @@ +{ "id": "rest", + "name": "Rest - Workout Timer App", + "shortName":"Rest", + "version": "0.01", + "description": "Rest timer and Set counter for workout, fitness and lifting things.", + "icon": "app.png", + "screenshots": [{"url": "screenshot1.png"}, {"url": "screenshot2.png"}, {"url": "screenshot3.png"}], + "tags": "workout,weight lifting,rest,fitness,timer", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"rest.app.js","url":"rest.app.js"}, + {"name":"rest.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/rest/rest.app.js b/apps/rest/rest.app.js new file mode 100644 index 000000000..aeb066e55 --- /dev/null +++ b/apps/rest/rest.app.js @@ -0,0 +1,322 @@ + +function roundRect (x1, y1, x2, y2, halfrad) { + const fullrad = halfrad + halfrad + const bgColor = g.getBgColor(); + const fgColor = g.getColor(); + g.fillRect(x1, y1, x2, y2); + g.setColor(bgColor).fillRect(x1, y1, x1 + halfrad, y1 + halfrad); + g.setColor(fgColor).fillEllipse(x1, y1, x1 + fullrad, y1 + fullrad); + g.setColor(bgColor).fillRect(x2 - halfrad, y1, x2, y1 + halfrad); + g.setColor(fgColor).fillEllipse(x2 - fullrad, y1, x2, y1 + fullrad); + + g.setColor(bgColor).fillRect(x1, y2-halfrad, x1 + halfrad, y2); + g.setColor(fgColor).fillEllipse(x1, y2-fullrad, x1 + fullrad, y2); + g.setColor(bgColor).fillRect(x2 - halfrad, y2-halfrad, x2, y2); + g.setColor(fgColor).fillEllipse(x2 - fullrad, y2-fullrad, x2, y2); +} + +function center(r) { + return {x: r.x + (r.x2 - r.x)/2 + 1, y: r.y + (r.y2 - r.y)/2 + 1} +} +function inRect(r, xy) { + return xy.x >= r.x && xy.x <= r.x2 && xy.y >= r.y && xy.y <= r.y2; +} + +let restSeconds = 60; +let setsCount = 3; + +let currentSet = 1; +let restUntil = 0; + +Bangle.loadWidgets(); + +const m = 2; // margin +const R = Bangle.appRect; +const r = {x:R.x+m, x2:R.x2-m, y:R.y+m, y2:R.y2-m}; +const s = 2; // spacing +const h = r.y2 - r.y; +const w = r.x2 - r.x; +const cx = r.x + w/2; // center x +const cy = r.y + h/2; // center y +const q1 = {x: r.x, y: r.y, x2: cx - s, y2: cy - s}; +const q2 = {x: cx + s, y: r.y, x2: r.x2, y2: cy - s}; +const q3 = {x: r.x, y: cy + s, x2: cx - s, y2: r.y2}; +const q4 = {x: cx + s, y: cy + s, x2: r.x2, y2: r.y2}; +const quadrants = [q1,q2,q3,q4]; +const c1 = center(q1) +const c2 = center(q2) +const c3 = center(q3) +const c4 = center(q4) + +const GREY_COLOR = '#CCCCCC'; +const SET_COLOR = '#FF00FF'; +const SET_COLOR_MUTED = '#FF88FF'; +const REST_COLOR = '#00FFFF'; +const REST_COLOR_MUTED = '#88FFFF'; +const RED_COLOR = '#FF0000'; +const GREEN_COLOR = '#00FF00'; +const GREEN_COLOR_MUTED = '#88FF88'; +const BIG_FONT = "6x8:2x2"; +const HUGE_FONT = "6x8:3x3"; +const BIGHUGE_FONT = "6x8:6x6"; + +function drawMainMenu(splash) { + g.setColor(REST_COLOR); + roundRect(q1.x, q1.y, q1.x2, q1.y2, 20); + g.setColor(SET_COLOR); + roundRect(q2.x, q2.y, q2.x2, q2.y2, 20); + g.setColor(GREY_COLOR); + roundRect(q3.x, q3.y, q3.x2, q3.y2, 20); + g.setColor(GREEN_COLOR); + roundRect(q4.x, q4.y, q4.x2, q4.y2, 20); + g.setColor(-1) + + if (splash) { + g.setFont(BIGHUGE_FONT).setFontAlign(0,0).drawString("R", c1.x, c1.y) + g.setFont(BIGHUGE_FONT).setFontAlign(0,0).drawString("E", c2.x, c2.y) + g.setFont(BIGHUGE_FONT).setFontAlign(0,0).drawString("S", c3.x, c3.y) + g.setFont(BIGHUGE_FONT).setFontAlign(0,0).drawString("T", c4.x, c4.y) + } else { + g.setFont("6x8").setFontAlign(0,0).drawString("Tap to\nConfigure", c1.x, c1.y-25) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString(restSeconds+ "s", c1.x, c1.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("REST", c1.x, c1.y + 25) + + g.setFont("6x8").setFontAlign(0,0).drawString("Tap to\nConfigure", c2.x, c2.y-25) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString(setsCount, c2.x, c2.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("SETS", c2.x, c2.y + 25) + + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("JUST\nDO\nIT", c3.x, c3.y) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString("GO", c4.x, c4.y) + } +} + +function drawSetRest() { + g.setColor(REST_COLOR); + roundRect(q1.x, q1.y, q1.x2, q1.y2, 20); + g.setColor(RED_COLOR); + roundRect(q3.x, q3.y, q3.x2, q3.y2, 20); + g.setColor(GREEN_COLOR); + roundRect(q4.x, q4.y, q4.x2, q4.y2, 20); + g.setColor(-1) + g.setFont("6x8").setFontAlign(0,0).drawString("Tap to\nConfirm", c1.x, c1.y-25) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString(restSeconds+ "s", c1.x, c1.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("REST", c1.x, c1.y + 25) + // g.setFont(BIG_FONT).setFontAlign(0,0).drawString("OK", c2.x, c2.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("-", c3.x, c3.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("+", c4.x, c4.y) +} + +function drawSetSets() { + g.setColor(SET_COLOR); + roundRect(q2.x, q2.y, q2.x2, q2.y2, 20); + g.setColor(RED_COLOR); + roundRect(q3.x, q3.y, q3.x2, q3.y2, 20); + g.setColor(GREEN_COLOR); + roundRect(q4.x, q4.y, q4.x2, q4.y2, 20); + g.setColor(-1) + g.setFont("6x8").setFontAlign(0,0).drawString("Tap to\nConfirm", c2.x, c2.y-25) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString(setsCount, c2.x, c2.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("SETS", c2.x, c2.y + 25) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("-", c3.x, c3.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("+", c4.x, c4.y) +} + +function drawExercise() { + g.setColor(REST_COLOR_MUTED); + roundRect(q1.x, q1.y, q1.x2, q1.y2, 20); + g.setColor(SET_COLOR); + roundRect(q2.x, q2.y, q2.x2, q2.y2, 20); + g.setColor(GREEN_COLOR_MUTED); + roundRect(q4.x, q4.y, q4.x2, q4.y2, 20); + g.setColor(-1); + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("SET", c2.x, c2.y-25) + g.setFont(HUGE_FONT).setFontAlign(0,0).drawString("#"+currentSet, c2.x, c2.y) + g.setFont(BIG_FONT).setFontAlign(0,0).drawString("PUSH >\nBUTTON\nWHEN\nDONE", c4.x, c4.y) +} + +function circlePoints (cx, cy, r, points) { + let circlePoints = []; + for (let i=0; i> 1) << 1 + const poly = smallQ3Circle.slice(0, circleParts + 2) + g.setColor(SET_COLOR); + g.fillPoly(poly); + + g.setColor(GREY_COLOR); + roundRect(q3.x, q3.y, q3.x2, q3.y2, 20); + g.setColor(-1).setFont(BIG_FONT).setFontAlign(0,0).drawString("REST", c3.x, c3.y) + + g.setColor(0); + g.setFont("6x8").drawString("Push button\nto skip ->", c4.x, c4.y); + + if (secondsRemaining > 0) { + if (secondsRemaining < 5) { + if (secondsRemaining > 1) { + Bangle.buzz(100); + } else { + Bangle.buzz(1000); + } + } + const renderTime = Date.now() - start; + setTimeout(redrawApp, Math.max(10, 1000 - renderTime)); + } else { + currentSet += 1; + if (currentSet > setsCount) { + currentSet = 1; + setMode(MAIN_MENU); + } else { + setMode(EXERCISE); + } + redrawApp(); + } +} + +function drawDoIt() { + const oldBgColor = g.getBgColor(); + g.setBgColor('#00FF00').clear(); + g.drawImage(getImg(), 44, 44); + g.setFont(BIG_FONT) + g.setColor(0); + setTimeout(() => { + g.setFontAlign(0, 0) + g.drawString('just ', R.x2/2, 20); + Bangle.buzz(150, 0.5); + }, 200); + setTimeout(() => { + g.drawImage(getImg(), 22, 44, {scale: 1.5}); + g.drawString(' DO ', R.x2/2, 20); + Bangle.buzz(200); + }, 1000); + setTimeout(() => { + g.drawString(' IT', R.x2/2, 20); + Bangle.buzz(200); + }, 1400); + setTimeout(() => { + setMode(MAIN_MENU); + g.setBgColor(oldBgColor); + redrawApp(); + }, 2000); +} + +const MAIN_MENU = 'MAIN_MENU'; +const SET_REST = 'SET_REST'; +const SET_SETS = 'SET_SETS'; +const EXERCISE = 'EXERCISE'; +const REST = 'REST'; +const DOIT = 'DOIT'; + +let mode = MAIN_MENU; + +function setMode(newMode){ + mode = newMode; +} + +function getImg() { + return require("heatshrink").decompress(atob("rFYwcBpMkyQCB6QFDmnStsk6dpmmatO2AoMm7VpkmapMm6Vp02TEAmSCIIFB2mbEYPbtu07VJmwFCzYRD0gdB0gmBEAgCCtoOBtIOBIIPTpo1BHwJQCAQMmydNI4RBFLIILDmnaps2L4Om7ZEBI4IgCAQNN0g+GJQKJDKwIaB0iJCJQQmBCgWmHAIdEHYKnFDQSbBkBcE0wOBFgImBSoMmQZJTE6VAbYMJPQRHBDQKMBmmTtoUCEBPSJQT8CgKPCcAJQEIILFHMohxDEAUANwZ9E0wdBUhDLGyAgDO4LIByYOBAQLpEL45KEm2AQIMkwEEYQZTB7Vt23TC4wCHCgOAgRUBEAL+CzVtkwRCHw4CJEANNm2QggXEX4jpBIJgCBgESOoKHB6RiByYCBDQSGCMoIdJHAQgCkmCgALCZALpCd4RiNYoKkCkESpC8CEYm2QByDDgEBkETpBWDtukKYZBOHAKkBgIGBIIRNC0wFEIKCDCyVEBASbLAReQEAXSghKCzQ7BQYIgUoAGBEARuDIKmSgAAByAgFASwgCgALFmikUEBRBYgggcwBBDtDrDASwfDgFIgAgYkAfDgVAgEJECw6BAAcSEAKGXDIUAhEgZIcEYS4ABAwwgUyAgFAwjIUDIifBdQggUDIkBZIjKBECYZEAA4gSHQogoRYIgQD5gghgIgQpAg/QeAgRQcNAggeLECQDBwAgryIgTxAgKwAgQpQgKgMhkmQIKcIIJEgEA+kEBNApMgdJBhBgkQIKFCpMAEBUAMQ+aIJUioAgKIItpIJkCEBEAIJIgKhIgMyRBFmikLMRMAgkEEAmTUhogRARlAhIggkAgLUiNIpMgD5AgWXQIgcpMJED8BEBmAED0kwIgRkAgLkAgSkMkwAhKxIgRkgggXIIcFgIEDaYIgRwggGgBKDECcEyVAgEQEIkSpIgUgADCQwzSBEC0gD4pBBkQdQDgYgIBAIgVHAJFBcYgMBgQgUPQIgFFINIBQQgQTYYgfXQIgFFYggPGgIVCgmQDogFCECr8CII4KCECUBED4AKFYQgOoAYFggIGEC4XDEDgLDkAgVD4kCBYgKEECsSBYmAEDILFEEGQEBYA==")); +} + +const onTouchPerQuadrantPerMode = { + // mode -> [[nextMode on touch, custom function], ... for all quadrants] + MAIN_MENU: [ + [SET_REST, null], [SET_SETS, null], + [DOIT, null], [EXERCISE, null] + ], + SET_REST: [ + [MAIN_MENU, Bangle.buzz], [null, null], + [null, () => { + restSeconds = Math.min(120, Math.max(0, restSeconds - 15)); + Bangle.buzz(100); + }], + [null, () => { + restSeconds = Math.min(120, Math.max(0, restSeconds + 15)); + Bangle.buzz(100); + }], + ], + SET_SETS: [ + [null, null], [MAIN_MENU, Bangle.buzz], + [null, () => { + setsCount = Math.min(15, Math.max(0, setsCount - 1)); + Bangle.buzz(100); + }], + [null, () => { + setsCount = Math.min(15, Math.max(0, setsCount + 1)); + Bangle.buzz(100); + }], + ], + EXERCISE: [ + [null, null], [null, null], + [null, null], [null, null], + ], + REST: [ + [null, null], [null, null], + [null, null], [null, null], + ] +} + +const drawFuncPerMode = { + MAIN_MENU: drawMainMenu, + SET_REST: drawSetRest, + SET_SETS: drawSetSets, + EXERCISE: drawExercise, + REST: drawRest, + DOIT: drawDoIt, +} + +function redrawApp(){ + g.clear(); + Bangle.drawWidgets(); + drawFuncPerMode[mode](); +} + +function buttonPress () { + if (mode === EXERCISE) { + setMode(REST); + restUntil = Date.now() + (restSeconds * 1000); + redrawApp(); + return; + } + if (mode === REST) { + restUntil = Date.now(); // skipping rest! + redrawApp(); + return; + } +} + +setWatch(buttonPress, BTN, { repeat: true, debounce: 25, edge:"falling"}); + +Bangle.on('touch', (button, xy) => { + for (let qidx=0; qidx<4; qidx++) { + if (inRect(quadrants[qidx], xy)) { + const nextMode = onTouchPerQuadrantPerMode[mode][qidx][0]; + const func = onTouchPerQuadrantPerMode[mode][qidx][1]; + if (func) func(); + if (nextMode) setMode(nextMode); + redrawApp(); + } + } +}); + +g.clear(); +drawMainMenu(true); +setTimeout(redrawApp, 1000); + diff --git a/apps/rest/screenshot1.png b/apps/rest/screenshot1.png new file mode 100644 index 000000000..616a7232b Binary files /dev/null and b/apps/rest/screenshot1.png differ diff --git a/apps/rest/screenshot2.png b/apps/rest/screenshot2.png new file mode 100644 index 000000000..e878fea96 Binary files /dev/null and b/apps/rest/screenshot2.png differ diff --git a/apps/rest/screenshot3.png b/apps/rest/screenshot3.png new file mode 100644 index 000000000..28d94eb91 Binary files /dev/null and b/apps/rest/screenshot3.png differ diff --git a/apps/runplus/karvonen.png b/apps/runplus/karvonen.png new file mode 100644 index 000000000..deacf7a8a Binary files /dev/null and b/apps/runplus/karvonen.png differ diff --git a/apps/supaclk/ChangeLog b/apps/supaclk/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/supaclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/supaclk/README.md b/apps/supaclk/README.md new file mode 100644 index 000000000..fc8f1a096 --- /dev/null +++ b/apps/supaclk/README.md @@ -0,0 +1,25 @@ +# SUPACLOCK Pro ULTRA + +A nice clock, with four ClockInfo areas at the bottom. Tap them and swipe up/down and left/right to toggle between different information. + + - Supports Light and Dark Themes. + - It has a useless splash-screen for increased ULTRAness + - Lazy Loading of Clock-Info, shows clock-face faster + - Uses locale module to display of day and month + + +Based on [LCD Clock Plus](https://banglejs.com/apps/?id=lcdclockplus) + +## Screenshots + +Light theme + +![light](screenshot.png) + +Dark theme + +![dark](screenshot2.png) + +## Credits + +Written by devsnd diff --git a/apps/supaclk/app-icon.js b/apps/supaclk/app-icon.js new file mode 100644 index 000000000..77598357b --- /dev/null +++ b/apps/supaclk/app-icon.js @@ -0,0 +1 @@ +atob("MDAB/////////////////////////////////gX2D8H//f328z3//f32+v3//AX2+v3///X2BgH///bm/v3/+A8e/v3//////////hf4/C+/+ffnM+5/+/ff1+3/+/ff1+H/+/ff1+7/+ffvt+9//BAweC+//////////4B//////AAf////+P+P////8+fH////48/H////45/H////8R/F////8x+A/////j8A/8H//j4APwD//AAYDjD//EA5HHD/+EByOHDf+EDkOGCf8OfkAAA/8P/4AAB/4P/8AAD/4f/8HAP/+///////////////55OAADx/55OTBxh/55OTD5k/55PTQDM/55PzxnAf4BAzzyOf8DAT7yff////////") diff --git a/apps/supaclk/app.js b/apps/supaclk/app.js new file mode 100644 index 000000000..f0bf32ae5 --- /dev/null +++ b/apps/supaclk/app.js @@ -0,0 +1,195 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are globalj +let removeHasNotRun = true; +let drawTimeout; + +const supaClockImg = { + width : 95, height : 13, bpp : 1, + buffer : atob("wL7B+Dhf4/C+4P//f328z0+/OZ9zPv/+/vt9fr99/X7e3f/8Bfb6/X77+vw9e///6+wMAv339fu+4DD/25v79Pv32/e4HNvAePf37BAweC+3+7f////////////P4B////////////////////////////7UEYf///////////2qrW////////////tdxh////////////iLtaA="), + palette: new Uint16Array(g.theme.dark ? [g.toColor("#fff"), 0] : [0, g.toColor("#fff")]), +} +// todo +// const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + +const interpolatePos = function(pos1, pos2, factor, easing) { + if (easing !== undefined) { + factor = Math.pow(factor, easing); + } + return {x: (pos1.x*(1-factor) + pos2.x*factor)/2, y: (pos1.y*(1-factor) + pos2.y*factor)/2} +} + +let drawSplashScreen = function (frame, total) { + const R = Bangle.appRect; + g.reset().setColor(g.theme.fg).setBgColor(g.theme.bg); + const startPos = {x: -200, y: R.h/2}; + const endPos = {x: R.x2 - supaClockImg.width*2 + 30, y: R.h/2}; + const pos = interpolatePos(startPos, endPos, frame/total, 0.1) + g.setFontAlign(0, 1).setColor('#888').setFont("4x6:3").drawString('ULTRA', 100, frame*18); + g.setFontAlign(0, 1).setColor('#888').setFont("4x6:3").drawString('PRO', 40, R.x2 - frame*18); + g.clearRect(0, pos.y-5, R.x2, pos.y + supaClockImg.height+25); + var date = new Date(); + let minutes = date.getMinutes(); + minutes = (minutes < 10 ? '0' : '') + minutes; + let hours = date.getHours()+''; + g.drawImage(supaClockImg, pos.x, pos.y, {scale: 2}); + g.setColor(0).setFont('6x8:2').setFontAlign(0, 1).drawString(hours + ':' + minutes, R.x2/2, pos.y + supaClockImg.height + 25) +} + +// for fast startup-feeling, draw the splash screen once directly once +g.clear() +drawSplashScreen(0, 20); + +let splashScreen = function () { + g.clearRect(R.x,R.y, R.x2, R.y2); + return new Promise((resolve, reject) => { + let frame = 0; + function tick() { + if (removeHasNotRun) drawSplashScreen(frame, 20); + frame += 1; + if (!removeHasNotRun) { + reject(); + } else if (frame < 20) { + setTimeout(tick, 50); + } else { + resolve(); + } + } + tick(); + }) +} + + +Graphics.prototype.setFontPlayfairDisplay = function() { + // https://www.espruino.com/Font+Converter + // + // 60pt, 2bpp, Numeric + // Actual height 62 (67 - 6) + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AD8/A40B/4IGh/8DI/4BA3/8AHFg//wAIFj/+ES8DEQ5FIj4ZGBAKdzhwIHNA8f4BoGUpBwGg/wRQ7HHPA0BFJA6HJY9/FI46GgLWHMiF/Mi8Dbo8MbuYALv6VGg//BA1/BAwQB/51Fn4IBNokBA4P/UAkPBASYEFQIABDI7DEDIY9EDIY9DDIY0EJoICDJoYNCFYgnDAYcDJQUBMAbKDBgcAuADCgw8DBgcYBg0AkAMGgAVDsADCgQiDLYYVDg4ZDCocOBgYiDnwDGNocHNAh/CRYy3Gj6uLcYgZHW4f8BAYrDDIjaDA4Y0DGYjJBbIoIDFQhGDCAoRBCAwAqcYcEBAbNDiDNHoCLDZoUDEQ8MBAccAYVwOAigCOQbaBToylDPYicC//waI4iVegYiDdYYiEdYYiEHgYiECAZFEDobbGRYoADRYgADVwgADVwYAjcYUDaoQAB8ACBjxcEAYX4BAc4DIQUCSgJtCj4iDhwDC/wZGg4iDgIeCn4iDGYS6BEQc8agQiEVQV/EQcDHgMDv4iDh4CBnIiEnwqB84iDgIeBj8fEQcH+AKBEQkf+E/8IiEv7qBg4iEfYQiFBAPgEQoIBNAs/DIIiEgAZCEQkDBAI3BRYn//AiFFYPAEQs/AoIiED4KVBEQg0BwAiFdASuFCYYiEJAYiEPwYHGAFsDAYUOBAcIAYVwBAdgCgRtDgIECjgQDgwDCMgkYVwSHEEQQZBCwQnDUgM4IIkD4AkDvACBh4WDgINBgE8gEMBoYLBEQMwBoY8B4AWCBoTfBwEPEQICCb4MAZ4V+EQX/h/cGwLSCg///98gE/HgUf//9/gMBNYU///nCYLsDAoOfAQJiCeIP8v4IBCAUP//wA4P8BAQrBwYZEJwIyC/6hDDIITBDIZXBwA/BDIZbCGYgRBCYRNDCYYqEDgoAXgYIHd4IAGXwQAEZgIIGYYIqGOAYADj4iH/4iGVAIQGuYiGgePEQ0cdQaVD4AiGhxFHuEPEQsDYAIiFjBOBFQwiGhxXBEQtwgAiFZ4IACCQbyBAAQSCdIIADQAgACGod/EQ0HDIgiCFQgiCFQoiCDIp7GaI5oGH4TRGFwIZHEQ9/EQwZBEQwAcggIHiAIHoA7EwBzBVwn+AgMMaAfAmAFBAQRlCDIMAVwfwgyiCEQQZBjAEBjgiDgHgAoNwEQcDHgQlCEQMOAgMeEQk4AgN4EQ0DEoQiCIQMfDIQiBh5rBXAYiBn7bEEQX/PgYiDfgJ8CEQYIBaQYiCBAJ5CEQYZEEQIpB//4EQkHDIjxCWAhaBEQIrBGYd/LYN/JoYAD/5nDbYiBCAAgQGAFReBHYp4CLwZ6CBAysCDQp6BRQghDAAJ5DXoQABDIaIBWwoqDRYgqDbIoADGgTFCGgoZBD4MHFYYeBKgMBcQQMBgZSCMAUf4EYBAQiCDoNgCwRNC8AZCgEMDIQEDgEgAQN4gE4EQkDKIIwChwCDEgIFBIoQXBh5uBj5RCDIK3CNAQ/CRYpUBSoaLCDgKEDHgSeEQQRUCcYpZCaIzbDTgbKEfog0DA4gICM4QIFFQgAih4pHn6ICAAn/VwReFEQ4ZHn5uFEQTCBESIMEEQd/TwYiCQgIVGYQIVCEQSwCLYQiCaYR1CEQLKFEQSFBEQzkCLYQiBaQRFFfwoiBAIPwgxoEj7iFEQJ7G//HV4ogBnzRHYA0/IIbRMCA8PZA8/A40ACA4AUvDlKAAl/BAcHAgKlBRgYNCcIiBBX4a+Cj4WBX4ThCv4WBDILhEQQICBGgQZBVwLQEDIP/z/gv6XBgLwCBwMfFYK1BAAIWBjySCFAk4AQPgIwQFBsA8BCQQoCEQMMBARdBgQTBkA+CBwMIBAINBGgYOBEQJHBMwQiDQgcGH4aBBBAMYBAJCBGgIDBH4MD/h7C/EPEQKZCMQQtCLwSFCRYnhPgS3CAgKoCVwi/DPgQFB8CXCDIQFBXQbrCj4VBSwiyDRoYAEv4zCBApnBAAp4CAExZCAApeECAgIGSwJWFSYTJBAAbADDIyXBMQYZCQQVgDIg0BgKRBDIY0BhwHBgIQCFYMwJogrBgKnCn4DBv+Ag48CIQU+gE8HgQuCnEAWAUcCgXgg4NCJARDBDIRIDg0B+D+CDIUwh48CEQfARod8DIUPQgaRCnL+DQQJrCDIZoDTwk/BAYZCRYYZERYf/JoSuE/5bCFYjSEW4aBCGgraHcZAqDBAYQFEYQHFAHh2Cj5XDg5UBS4JVETIMHUocARAU/NIcHO4SUEj4WBWILIECwKxBZAgiCW4YiIh4iDJwZFIv4NBh7rDNAiRkA=='))), + 46, + atob("ExouGyYjJiEoISgoFQ=="), + 70|65536 + ); +} + +Graphics.prototype.setFontPlayfairDisplaySm = function() { + // Actual height 18 (17 - 0) + // + // 18pt, 1bpp, ASCII capitals + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('ABU/weP/3j/E4BAMIgEH4EB8EAkEAjwICgGADYcQgFEgEX4EeoEBBANHgE9gEEBAYABjl8h+Gg/woM+iFP4n///En1Bw/wvH4CgIXBgfggP4glDg0QoUH+cA/MAgO+gGf4HEiHBwlAn8Ah5NCgeAgPwh3+g/j4MQuFEzkc+cHmXA7+wg/8gO3g9hwP44Fv8ETEARbCPoMfPoIIDgf8gf/4P//nwh9gBgUEAQPwg8f//D//An6gBgEUJ4SoBgP+gF/wAqBgfwgFIZAqaBEwMBAwKjB/4IGCIStDJ4ISBEgIADmEAhhXBAwLcBBAYhCAAcBBoNwXgYWDfwPAQgIgBDI0AW4Mf8EPQwOHgGAoEgiEMhiPBOAUPEoZbCoEfSAKhCMoZEDCQPA4Eg+EMvkD+75B4EcKAtAhEQgUIoODmFA/1wn/8h8+WgIpDb4M4gEWgEJwEMuEH/+D/7EBJwLQCR4Mg8kInMGh1Dgef4Hn8F4/EAjB/Ej/Aj/4h//gfh4OQiFGhkh/8In7uB4EOYAOAGQPwnH+h1/gewgDWBZILiDgfPwP7+F/gkT8MEvlB//wvv8ghtBAAMPFwPwgf+kHBxEgkkMj6qBwF/4EPNYYAB8PAnFwhzyERgIIBjrgBQgLDDAQIeBlEAicAglAgUQgGCgFgwAFBAAkSCIOAgK3BpAILGA1AwEIkECVgNEGoUCSwUAGIYgBLQLqB8WAnGBh0xwZxBgKjBeIMAjzSBsEQgkIgGEn0in/Iz/yo0IyMOkl/5Mf+FHIAMCgZIBv5PFgfAgcQg2Ah0gg/ogP/gE//gECj6aBH4MMDYMEgDXCv/8j//gkQoICBdIIIBg/v4HzOAN4G4X+HwPAn/4CIUB4KABoAdBgIsBS4Mgvl8KYosBGoUH/4aIoPAuAsBh/+gI1BSYIABCIIaCLIZWDoM/EYUcg8HQwI6DDRsfKYNAGAMHcgLaBh/wHwMD/+B+HwsBQDhFBWYNA/kIn0H5/AgQ1LVQZNBQAdBI44LBAAYUBR4gRBDoQAChKHBv//z//4//8IdEdJdAg5wB8ETFgMP4JsBoEcHwbOLFIQRCoEAuEAdIQ1EvhZB4EH/kAv/gAgU8gEcgE4AIJrNHwsBgfggF+YoPAgP4gE/VoLFBNYYbBcAMfa4nB4FwFgLXCKAOAmF4ngRDEoOAgaaEPpMCTYOEiFgg/4gJoBnyjBeoQjCj/6FgOHw/hwE4WwI+B4kAvPAn1//eP/3h/84j6JBa5tBBYND8BHC9/A/PwjAcBYoUAuPwn0Mj/BKANBj0QofEiH/gYLB8akBkBWBg+AgOAcYIyBgbFEHwKYFeIb+D//gv/4KAUB4CACBwQCBwP/4FAgEYGoaYBv/Ai/+AgUD+EAnEAuEE8AkCDQJsBg72BCoXwJwKxB4MB8Fw4EfwEH/hHCp4vCDYMBhxZCHwLpEsEMj0Hg/DoP9iFffoP4DgMBk/wuF8jEPMQJrCsBHFRgKPBgZHDh/wrgRCDQIACvEcjEfgi8Bj/wr/kj/hg/goPgmFgVYcAE4IAEJAIlCHwMGAgNwCQLGBNQL7CgTHCAAiCBAAQA='))), + 32, + atob("BAUHCwsNEQQGBgkKBQkFBwwHCgkKCQoJCgoFBQsLCwkQDAwNDgwLDQ8HBw0LEQ0PDA8NCwwMDBINCwsGBwY="), + 18|65536 + ); +} + +// Actually draw the watch face +let draw = function() { + g.reset().setColor(g.theme.fg).setBgColor(g.theme.bg); + g.clearRect(R.x,R.y, R.x2, R.y2 - 60); + var date = new Date(); + let minutes = date.getMinutes(); + minutes = (minutes < 10 ? '0' : '') + minutes; + + g.setColor(g.theme.fg) + // Time + const yt = R.y + 92 - 20 - 30 + 6 + 10; + const xt = R.w/2 - 5; + let hours = date.getHours()+''; + g.setFontAlign(1, 0).setFontPlayfairDisplay().drawString(hours, xt - 8, yt); + g.setFontAlign(0, 0).setFontPlayfairDisplay().drawString(':', xt, yt); + g.setFontAlign(-1, 0).setFontPlayfairDisplay().drawString(minutes, xt + 8, yt); + // logo + g.drawImage(supaClockImg, R.x2 - supaClockImg.width - 2, R.y + 2); + // dow + date + let dateStr = require("locale").dow(date).toUpperCase() + '\n' + require("locale").month(date, 2).toUpperCase() + ' ' + date.getDate(); + g.setFont('6x8').setFontAlign(-1, 0).drawString(dateStr, R.x2 - supaClockImg.width - 2, R.y + 42 - 30 + 8); + + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +}; + +let clockInfoDraw = (itm, info, options) => { + g.reset().setFontPlayfairDisplaySm().setColor(g.theme.fg).setBgColor(g.theme.bg); + if (options.focus) g.setBgColor("#FF0"); + g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h}); + + if (info.img) { + g.drawImage(info.img, options.x+1,options.y+2); + } + var text = info.text.toString().toUpperCase(); + if (g.setFontPlayfairDisplaySm().stringWidth(text)+24-2>options.w) g.setFont("4x6:2"); + g.setFontAlign(-1,-1).drawString(text, options.x+24+3, options.y+6); +}; + +let clockInfoDrawR = (itm, info, options) => { + g.reset().setFontPlayfairDisplaySm().setColor(g.theme.fg).setBgColor(g.theme.bg); + if (options.focus) g.setBgColor("#FF0"); + g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h}); + + if (info.img) { + g.drawImage(info.img, options.x + options.w-1-24,options.y+2); + } + var text = info.text.toString().toUpperCase(); + if (g.setFontPlayfairDisplaySm().stringWidth(text)+24-2>options.w) g.setFont("4x6:2"); + g.setFontAlign(1,-1).drawString(text, options.x+options.w-24-3, options.y+6); +}; + + +let clockInfoItems; +let clockInfoMenu1; +let clockInfoMenu2; +let clockInfoMenu3; +let clockInfoMenu4; + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { // for fastloading, clear the app memory + removeHasNotRun = false; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontPlayfairDisplay + delete Graphics.prototype.setFontPlayfairDisplaySm + clockInfoMenu1&&clockInfoMenu1.remove(); + clockInfoMenu2&&clockInfoMenu2.remove(); + clockInfoMenu3&&clockInfoMenu3.remove(); + clockInfoMenu4&&clockInfoMenu4.remove(); + }}); + +Bangle.loadWidgets(); + +let R = Bangle.appRect; +let midX = R.x+R.w/2; +let upperCI = R.y2-28-28; +let lowerCI = R.y2-28; + +g.clearRect(R.x, R.y, R.x2, R.y2); +splashScreen().then(() => { + g.clearRect(R.x, 0, R.x2, R.y2); + draw(); + Bangle.drawWidgets(); + // Allocate and draw clockinfos + g.setFontAlign(1, 1).setFont('6x8').drawString('Loading Clock Info Modules...', R.x + 10, upperCI); + setTimeout(() => { + // delay loading of clock info, so that the clock face appears quicker + g.clearRect(R.x, upperCI, R.x2, upperCI+10); // clear loading text + try { + clockInfoItems = require("clock_info").load(); + clockInfoMenu1 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDraw}); + clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+1, y:upperCI, w:midX-2, h:28, draw : clockInfoDrawR}); + clockInfoMenu3 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:R.x+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDraw}); + clockInfoMenu4 = require("clock_info").addInteractive(clockInfoItems, { app:"lcdclock", x:midX+1, y:lowerCI, w:midX-2, h:28, draw : clockInfoDrawR}); + } catch(err) { + if ((err + '').includes('Module "clock_info" not found' )) { + g.setFont('6x8').drawString('Please install\nclockinfo module!', R.x + 10, upperCI); + } + } + }, 1); +},() => {}); + +} diff --git a/apps/supaclk/app.png b/apps/supaclk/app.png new file mode 100644 index 000000000..54f2047a5 Binary files /dev/null and b/apps/supaclk/app.png differ diff --git a/apps/supaclk/metadata.json b/apps/supaclk/metadata.json new file mode 100644 index 000000000..016182545 --- /dev/null +++ b/apps/supaclk/metadata.json @@ -0,0 +1,16 @@ +{ "id": "supaclk", + "name": "SUPACLOCK Pro ULTRA", + "version": "0.01", + "description": "SUPACLOCK Pro ULTRA, with four ClockInfo areas at the bottom. Tap them and swipe up/down and left/right to toggle between different information.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], + "type": "clock", + "tags": "clock,clkinfo,clockinfo", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "dependencies" : { "clock_info":"module" }, + "storage": [ + {"name":"supaclk.app.js","url":"app.js"}, + {"name":"supaclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/supaclk/screenshot.png b/apps/supaclk/screenshot.png new file mode 100644 index 000000000..54f2047a5 Binary files /dev/null and b/apps/supaclk/screenshot.png differ diff --git a/apps/supaclk/screenshot2.png b/apps/supaclk/screenshot2.png new file mode 100644 index 000000000..46d04a166 Binary files /dev/null and b/apps/supaclk/screenshot2.png differ diff --git a/apps/thunder/ChangeLog b/apps/thunder/ChangeLog new file mode 100644 index 000000000..3513ca80e --- /dev/null +++ b/apps/thunder/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Use locale module to display distance +0.03: Fix lightning image and flash button diff --git a/apps/thunder/README.md b/apps/thunder/README.md new file mode 100644 index 000000000..86607107e --- /dev/null +++ b/apps/thunder/README.md @@ -0,0 +1,20 @@ +# Come on Thunder + +Simple timer to calculate how far away lightning is. + +A tribute to [Flash Boom](https://archive.org/details/tucows_33513_Flash_Boom), the greatest app of all time! + +![](screenshot.png) + +## Usage + +Use the "Flash" button when you see the flash of lightning, and press the "Boom" button when you hear the rumble of thunder! + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) + +## Icons + +- [Cloud Lightning](https://icons8.com/icon/41144/cloud-lightning) +- [Lightning](https://icons8.com/icon/60998/lightning-bolt") diff --git a/apps/thunder/app-icon.js b/apps/thunder/app-icon.js new file mode 100644 index 000000000..7a95e1390 --- /dev/null +++ b/apps/thunder/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AH4AO/uEkETxoWR/1QN4deC6HBgN89/suMB54WO/gqFsEcC53hg4GE/ECC51Qr4HFsF/RhsB94IF+MPC5n4IwoAB+8HscRk2vC5Hzhh/IAAUC34XH+UOBI8Dzv/7MwHo//+BWIYAf6gB9F/tziBuN4ELTgpSBI5AAE/TGE+kBvv/NJAAFmAPDsEtchwAByFvaYffC6HQl7KDKo5mBx6fHj7TChokHgEIW5AXC+KjHkAuII4gXH+ouJO4hHHwAuJU4h3G/YuKa4inG8IuK4ELAodwa4kwUoMAz6qCLIX5gF/CIf0gN8AgP2CwUKVQUGBQPZmDRGuASBp4FB2EB56qCt/0BgMCOoQAD/tziAXCkEdVQQuBHoMmCwwAF+0C96qCLoQAO4E+//8LoYAPmiqDFyP4r6qFUIgAK66qFhIvPUYJjDgGvMCLzDjYWU/EB74XU6EuCyn/oYWV/NPC6vnCyqqFAH4A//4A==")) diff --git a/apps/thunder/app.js b/apps/thunder/app.js new file mode 100644 index 000000000..4544b224d --- /dev/null +++ b/apps/thunder/app.js @@ -0,0 +1,124 @@ +Bangle.loadWidgets(); +g.clear(true); +Bangle.drawWidgets(); + +Bangle.setLCDTimeout(undefined); + +let renderIntervalId; +let startTime; + +const locale = require("locale"); + +const DEFAULTS = { + units: 0, +}; + +const settings = require("Storage").readJSON("thunder.json", 1) || DEFAULTS; + +var Layout = require("Layout"); +var layout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"", id:"time", fillx:1}, + {type:"txt", pad:8, font:"20%", label:"", id:"distance", fillx:1}, + {type:"h", c: [ + {type:"btn", font:"6x8:2", label:"Flash", cb: l=>flash() }, + {type:"img", pad:4, src:atob("DhoBH+B/gf4P8D/A/gP4H+B/AfwH8B//////7/+A/APwD4A+APADwA4AcAHABgAYAA==") }, + {type:"btn", font:"6x8:2", label:"Boom", cb: l=>boom() } + ]}, + ] +}, { + lazy: true, + back: load, +}); + +// TODO The code in this function appears in various apps so it might be +// nice to add something to the time_utils module. (There is already a +// formatDuration function but that doesn't quite work the same way.) +const getTime = function(milliseconds) { + let hrs = Math.floor(milliseconds/3600000); + let mins = Math.floor(milliseconds/60000)%60; + let secs = Math.floor(milliseconds/1000)%60; + let tnth = Math.floor(milliseconds/100)%10; + let text; + + if (hrs === 0) { + text = ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2) + "." + tnth; + } else { + text = ("0"+hrs) + ":" + ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2); + } + + return text; +}; + +// Convert milliseconds to distance based on one km in 2.91 s or +// one mile in 4.69 s +// See https://en.wikipedia.org/wiki/Speed_of_sound +const getDistance = function(milliseconds) { + let secs = milliseconds/1000; + let distance; + + if (settings.units === 1) { + let kms = Math.round((secs / 2.91) * 10) / 10; + distance = kms.toFixed(1) + "km"; + } else if (settings.units === 2) { + let miles = Math.round((secs / 4.69) * 10) / 10; + distance = miles.toFixed(1) + "mi"; + } else { + let meters = (secs / 2.91) * 1000; + distance = locale.distance(meters); + } + + return distance; +}; + +const renderIntervalCallback = function() { + if (startTime === undefined) { + return; + } + + updateTimeAndDistance(); +}; + +const flashAndBoom = function() { + Bangle.buzz(50, 0.5); + + if (renderIntervalId !== undefined) { + clearInterval(renderIntervalId); + renderIntervalId = undefined; + } +}; + +const flash = function() { + flashAndBoom(); + + startTime = undefined; + updateTimeAndDistance(); + + startTime = Date.now(); + renderIntervalId = setInterval(renderIntervalCallback, 100); +}; + +const boom = function() { + flashAndBoom(); + + updateTimeAndDistance(); + startTime = undefined; +}; + +const updateTimeAndDistance = function() { + let t; + + if (startTime === undefined) { + t = 0; + } else { + t = Date.now() - startTime; + } + + layout.time.label = getTime(t); + layout.distance.label = getDistance(t); + + layout.render(); + // layout.debug(); +}; + +updateTimeAndDistance(); diff --git a/apps/thunder/app.png b/apps/thunder/app.png new file mode 100644 index 000000000..5767e9275 Binary files /dev/null and b/apps/thunder/app.png differ diff --git a/apps/thunder/metadata.json b/apps/thunder/metadata.json new file mode 100644 index 000000000..52abcf4d8 --- /dev/null +++ b/apps/thunder/metadata.json @@ -0,0 +1,18 @@ +{ "id": "thunder", + "name": "Come on Thunder", + "shortName":"Thunder", + "version":"0.03", + "description": "Simple timer to calculate how far away lightning is", + "icon": "app.png", + "tags": "tool,weather", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{ "url": "screenshot.png" }], + "allow_emulator": true, + "storage": [ + {"name":"thunder.app.js","url":"app.js"}, + {"name":"thunder.settings.js","url":"settings.js"}, + {"name":"thunder.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"thunder.json"}] +} diff --git a/apps/thunder/screenshot.png b/apps/thunder/screenshot.png new file mode 100644 index 000000000..3fc369976 Binary files /dev/null and b/apps/thunder/screenshot.png differ diff --git a/apps/thunder/settings.js b/apps/thunder/settings.js new file mode 100644 index 000000000..1a2959227 --- /dev/null +++ b/apps/thunder/settings.js @@ -0,0 +1,40 @@ +(function (back) { + const FILE = "thunder.json"; + const DEFAULTS = { + units: 0, + }; + + let settings = {}; + + const loadSettings = function() { + settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; + }; + + const saveSettings = function() { + require('Storage').writeJSON(FILE, settings); + }; + + const showMenu = function() { + const unitOptions = [/*LANG*/'Auto','kms','miles']; + + const menu = { + '': {'title': 'Thunder'}, + '< Back': back, + 'Units': { + value: 0|settings.units, + min: 0, + max: unitOptions.length-1, + format: v => unitOptions[v], + onchange: v => { + settings.units = 0 | v; + saveSettings(settings); + } + }, + }; + + E.showMenu(menu); + }; + + loadSettings(); + showMenu(); +}); diff --git a/apps/twotwoclock/ChangeLog b/apps/twotwoclock/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/twotwoclock/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/twotwoclock/app-icon.js b/apps/twotwoclock/app-icon.js new file mode 100644 index 000000000..36d955549 --- /dev/null +++ b/apps/twotwoclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgcgyUEy/f5HwgENy3bl//AAkkyMky3f4vkDoMarUly/7CImBCIP/pvggFIgVJokSpO/CIUAgMk239t/9yVAgFBlEXGokfHgoAEjhbDn4RMv4DB/wRBwAXCg/8DokcuAEBvwRRg4RBg//AQIRBn4CCCKOcI40H+4RH3g7DnwRHI4efOAf+CJeSpX9k1JkxHHCIlP/9s5IRGFoRHCGon8CJccgP4jkPa4IRKzlxZAKzN7/xEAPwUIQRNxwRBfZRZECgIRKyVpgmypIXB5k/VIjSC7EQpMgyShBz+MnH/x0AAQP4gf//EGCIMSpMkz+AnBNBnHjx9/8f//c/5MkiwRB34RBjkOgHgh9/gf9/N/5M0GoIRB+AgC/H8IwX77c//M2CIW3+BZE+4CB/vbv/7k2W6EM2fQCIu///v7YGBk3/yENSooAHk3j+ENCBiYD9p9Ba5IPB4BOCPoIRLAAQRBAgWAg4ECCgIRBQYQRBLIYRIAAgRLHYkHLJojECJSiC75HHIgvbt5ZJCJRZGCIx9KCIwDC+IRBWaEfCKA1DABnty4RPtsCvcmdgLuDAA3btsAp8mAgO3CBH+BgMgp/2CJw=")) \ No newline at end of file diff --git a/apps/twotwoclock/app.js b/apps/twotwoclock/app.js new file mode 100644 index 000000000..57be691e1 --- /dev/null +++ b/apps/twotwoclock/app.js @@ -0,0 +1,159 @@ +/* +// to calculate 'numerals' by adding a border around text +require("Font4x5Numeric").add(Graphics); +function getImageForNumber(n) { + g.setFont("4x5Numeric:3x2"); + var sz = g.stringMetrics(n), s=1; + var b = Graphics.createArrayBuffer(12 + s*2, sz.height+s*2, 2, {msb:true}); + b.setFont("4x5Numeric:3x2").setFontAlign(0,-1); + b.transparent = 2; + b.setColor(b.transparent); + b.fillRect(0,0,b.getWidth(),b.getHeight()); + b.setColor(0); + var x = s+6, y = s; + for (var ix=-s;ix<=s;ix++) + for (var iy=-s;iy<=s;iy++) + b.drawString(n, x+ix, y+iy); + b.setColor(3); + b.drawString(n, x, y); + print('atob("'+btoa((b.asImage("string")))+'"),'); +} +for (var i=0;i<10;i++) + getImageForNumber(i); +*/ +{ + let numerals = [ + atob("DgyCAgAAAqP//yo///Kj8D8qPyPyo/I/Kj8j8qPyPyo/A/Kj//8qP//yoAAAKg=="), + atob("DgyCAqgAqqqPyqqo/Kqqj8qqqPyqqo/Kqqj8qqqPyqqo/Kqqj8qqqPyqqoAKqg=="), + atob("DgyCAgAAAqP//yo///KgAD8qAAPyo///Kj//8qPwACo/AAKj//8qP//yoAAAKg=="), + atob("DgyCAgAAAqP//yo///KgAD8qAAPyo///Kj//8qAAPyoAA/Kj//8qP//yoAAAKg=="), + atob("DgyCAgAgAqPyPyo/I/Kj8j8qPwPyo///Kj//8qAAPyqqo/Kqqj8qqqPyqqoAKg=="), + atob("DgyCAgAAAqP//yo///Kj8AAqPwACo///Kj//8qAAPyoAA/Kj//8qP//yoAAAKg=="), + atob("DgyCAgAAAqP//yo///Kj8AAqPwACo///Kj//8qPwPyo/A/Kj//8qP//yoAAAKg=="), + atob("DgyCAgAAAqP//yo///KgAD8qqqPyqqo/Kqqj8qqqPyqqo/Kqqj8qqqPyqqoAKg=="), + atob("DgyCAgAAAqP//yo///Kj8D8qPwPyo///Kj//8qPwPyo/A/Kj//8qP//yoAAAKg=="), + atob("DgyCAgAAAqP//yo///Kj8D8qPwPyo///Kj//8qAAPyoAA/Kj//8qP//yoAAAKg==") + ]; + + Graphics.prototype.setFontLECO1976Regular = function() { + // Actual height 24 (23 - 0) + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('ADX/+eA//3AQwUI/wCKByIAFh04HYP/GoICHj08gACFChYjCAAsf/FwAQPwAQsevHw/14/4CFBYUev4CGjk/+CSG4ACFwHh4ICB45HB55KCBwJ7BgP4gEDEQMHDQMfLIM/AQN9AQP54YgBAQPBBAMBAQjUJQZXzIIICMCIQCE8YCBgZKBd6gAJvxxBQ4JuB/H/86CD/kAn/gP4IZFBASOBDIJqCDQIgCEwQsCABESAQNzRwM/AQTOJBYd/CIMzLxUBCgMBJAICFh5dBARAUIAA0DfIQCHHaYCJaI4pBNIMHAQ4AICgUfwEBfAMPAQKyBSQbvCMhDJBCgICH+A1BARYaLfwxKBDqp0N/IRB/YCH+fHARn/ARHDKZnxExosGQCC8CAREDARosS54LBAQ5cPTAQCE8IsIHxQsg/ACH+ACNQaRWZFiXDBYIXCAQgsgh0DwEeg4CGCIoUE/EeAQ4UIgfgAQ0HUQICCh94AQcOnEAj08AQY7JCI4CXAAscjgOGh4vBI4UHuBWGMQsB4AmGPwQCI+P3wACG+YCBEAP/ART4G//4ARBJBvH3/4CKx4CDC4ICGVpALBwACI+ZHBARYaLFiXHARgXGv+A/0/FiXwg4CV8EDFj34h4RDv//gE//0Aj/8WaaGNAQpZXcBwCF+AjB8CDT+JTLHYQCE8ICBKyMDIgICLDRYsRYYx0CUILpLARA+dL4UPAQMfAQM/NAQCB/aVE4YCBwJrUJoICSEyBHCj/wDIP+CoQdBj4CEv5oBBwMf/AHBZz//BwKJCh/gfAIsBgBBCn6/edlnzLIICLU4QCIFI/AAQpKBARgRB/ACIFAN4AQhZM+KwB+LyB+P8AQKzB+IRC54CB44CB4ICB4CGH4YREAQnzARyYCAQnhWY5JBARLsMCg5tBbR8AfYICLDRgABb4S3C/4CCHIM/BIMHBIYXBj4CBv+AgYIBn62BC4QTCEYQpCLgTEBCIgGBj4mCAQU/EwhNCEIIQDEgP/GQIaCgJEEAoQLCFgQXCCwJBDCoOA8EDVoKGB/gdB/w7Bn4aBh50CW4IFCBYV+n4mBC4IdCEYQpCZAZQCAQ8DAQRNBAQwXKAQQAF8IXB+YCI44CM/4CI4ZWDAAV/f4IAIP4QCFuCLBDQTmCVoRZDSoMPZYqqBPYI4GFhA+IJQQXCA'))), + 32, + atob("CAcMDw8WEAcKCg0NBwoHCg8NDw8PDw8ODw8HBw0NDQ8RDw8PDw8ODw8HDg8NFBAPDhAPDw0PDxYPDw8JCgk="), + 24|65536 + ); + }; + Graphics.prototype.setFontLECO1976Regular14 = function() { + // Actual height 14 (13 - 0) + return this.setFontCustom( + atob('AAAAAAAAAAAD+w/sAAAAA8APAAAA8APAAAAMwP/D/wMwDMD/w/8DMAAAAAD8w/M8z/M/zPM/DPwAAAAPwD8QzcP/D/AHgD/D/wzMI/APwAAAAD/w/8MzDMwzMM/DPwDAAADwA8AAAAAAD8H/74f4BwAAAA4B/z8/8D8AAAAAeAPwD8AeAHgAAAAAAAAYAGAH4B+AGABgAAAAAAHgB4AAAAAYAGABgAYAAAAAABgAYAAAAQA8D/D+A8AAAAAA/8P/DAwwMMDD/w/8AAAAAwMMDD/w/8ADAAwAAO/DvwzMMzDMw/MPzAAAAAMDDMwzMMzDMw/8P/AAAAAPwD8ADAAwAMA/8P/AAAAAP3D9wzMMzDMwz8M/AAAAAP/D/wzMMzDMwz8M/AAA4AOADAAwAMAD/w/8AAAAA/8P/DMwzMMzD/w/8AAAAA/MPzDMwzMMzD/w/8AAAAAYYGGAAAAAGHhh4AABwAcAPgDYB3AYwAAAAAZgGYBmAZgGYBmAAAAABjAdwDYA+AHABwAAA4AOADOwzsMwD8A/AAAAAA//P/zAM37N+zZs/7P+wAAAAP/D/wzAMwDMA/8P/AAAAAP/D/wzMMzDMw/8P/AAAAAP/D/wwMMDDAwwMMDAAAAAP/D/wwMMDDhw/8H+AAAAAP/D/wzMMzDMwzMMDAAAAAP/D/wzAMwDMAzAMAAAA/8P/DAwzMMzDPwz8AAAAA/8P/AMADAAwD/w/8AAAAA/8P/AAAwMMDDAwwMMDD/w/8AAAAA/8P/AcAPAPwDvwj8AAAAA/8P/AAwAMADAAwAAP/D/w/AB+AHwA8B+B+A/8P/AAA/8P/D/wfAB8AHw/8P/AAAAAP/D/wwMMDDAw/8P/AAAAAP/D/wzAMwDMA/APwAAA/8P/DAwwMMDD/8//AAwAA/8P/DOAzwM/D9w/EAAAAA/MPzDMwzMMzDPwz8AADAAwAMAD/w/8MADAAwAAAD/w/8ADAAwAMP/D/wAAPAD8AP4AfAHwP4PwDgAAAPgD/gH8AfB/w/AP4A/wA8D/D/A8AAADAw4cP/A/APwH+DzwwMAAAAA/APwAPwD8A/D8A/AAAAAAz8M/DMwzMMzD8w/MAAAAA/////ADwA4ADwA/wB/ADwAAMAPAD////8A'), + 32, + atob("BAQHCQkNCQQGBggIBAYEBgkHCQkJCQkICQkEBAcIBwkKCQkJCQkICQkECAkHDAkJCAkJCQgJCQ0JCQkFBgU="), + 14|65536 + ); + }; + + // timeout used to update every minute + let drawTimeout; + + // schedule a draw for the next minute + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function() { + queueDraw(); + var d = new Date(); + var hr = d.getHours().toString().padStart(2,0); + var mn = d.getMinutes().toString().padStart(2,0); + var date = require("locale").date(new Date()).split(" ").slice(0,2).join(" ").toUpperCase(); + var x = 6, y = 16, w = 55, h = 67, datesz = 20, s=5; + g.reset(); + background.fillRect(x, y, x + w*2, y + h*2 + datesz); + var dx = x+w, dy = y+h+datesz-10; + g.setFont("LECO1976Regular").setFontAlign(0,0); + g.setColor(g.theme.bg).drawString(date, dx+3,dy-3).drawString(date, dx+3,dy+3); + g.drawString(date, dx-3,dy-3).drawString(date, dx-3,dy+3); + g.drawString(date, dx,dy-3).drawString(date, dx,dy+3); + g.drawString(date, dx-3,dy).drawString(date, dx+3,dy); + g.setColor(g.theme.fg).drawString(date, dx,dy); + g.drawImage(numerals[hr[0]], x, y, {scale:s}); + g.drawImage(numerals[hr[1]], x+w, y, {scale:s}); + g.drawImage(numerals[mn[0]], x, y+h+datesz, {scale:s}); + g.drawImage(numerals[mn[1]], x+w, y+h+datesz, {scale:s}); + }; + + let clockInfoMenuA, clockInfoMenuB; + // Show launcher when middle button pressed + Bangle.setUI({ + mode: "clock", + redraw : draw, + remove: function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (clockInfoMenuA) clockInfoMenuA.remove(); + if (clockInfoMenuB) clockInfoMenuB.remove(); + require("widget_utils").show(); // re-show widgets + } + }); + + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); + let R = Bangle.appRect; + let background = require("clockbg"); + background.fillRect(R); + draw(); + g.flip(); + + // Load the clock infos + let clockInfoW = 54; + let clockInfoH = g.getHeight()>>1; + let clockInfoItems = require("clock_info").load(); + let clockInfoDraw = (itm, info, options) => { + // itm: the item containing name/hasRange/etc + // info: data returned from itm.get() containing text/img/etc + // options: options passed into addInteractive + // Clear the background - if focussed, add a border + g.reset().setBgColor(g.theme.bg).setColor(g.theme.fg); + var b = 0; // border + if (options.focus) { // white border + b = 4; + g.clearRect(options.x, options.y, options.x+options.w-1, options.y+options.h-1); + } + background.fillRect(options.x+b, options.y+b, options.x+options.w-1-b, options.y+options.h-1-b); + // we're drawing center-aligned here + if (info.img) + require("clock_info").drawBorderedImage(info.img,options.x+3, options.y+3, {scale:2}); + g.setFont("LECO1976Regular").setFontAlign(0, -1); + var txt = info.text.toString().toUpperCase(); + if (g.stringWidth(txt) > options.w) // if too big, smaller font + g.setFont("LECO1976Regular14"); + if (g.stringWidth(txt) > options.w) {// if still too big, split to 2 lines + var l = g.wrapString(txt, options.w); + txt = l.slice(0,2).join("\n") + (l.length>2)?"...":""; + } + var x = options.x+options.w/2, y = options.y+54; + g.setColor(g.theme.bg).drawString(txt, x-2, y). // draw the text background + drawString(txt, x+2, y). + drawString(txt, x, y-2). + drawString(txt, x, y+2); + // draw the text, with border + g.setColor(g.theme.fg).drawString(txt, x, y); + }; + + clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, { + app:"pebblepp", + x : g.getWidth()-clockInfoW, y: 0, w: clockInfoW, h:clockInfoH, + draw : clockInfoDraw + }); + clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, { + app:"pebblepp", + x : g.getWidth()-clockInfoW, y: clockInfoH, w: clockInfoW, h:clockInfoH, + draw : clockInfoDraw + }); + } \ No newline at end of file diff --git a/apps/twotwoclock/icon.png b/apps/twotwoclock/icon.png new file mode 100644 index 000000000..a4258a7f3 Binary files /dev/null and b/apps/twotwoclock/icon.png differ diff --git a/apps/twotwoclock/metadata.json b/apps/twotwoclock/metadata.json new file mode 100644 index 000000000..ebcba539c --- /dev/null +++ b/apps/twotwoclock/metadata.json @@ -0,0 +1,16 @@ +{ "id": "twotwoclock", + "name": "TwoTwo Clock", + "shortName":"22 Clock", + "version":"0.01", + "description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos", + "icon": "icon.png", + "type": "clock", + "tags": "clock,clkinfo,clockbg", + "supports" : ["BANGLEJS2"], + "dependencies" : { "clock_info":"module", "clockbg":"module" }, + "screenshots": [{"url":"screenshot.png"}], + "storage": [ + {"name":"twotwoclock.app.js","url":"app.js"}, + {"name":"twotwoclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/twotwoclock/screenshot.png b/apps/twotwoclock/screenshot.png new file mode 100644 index 000000000..4d31d1520 Binary files /dev/null and b/apps/twotwoclock/screenshot.png differ diff --git a/apps/vpw_clock/ChangeLog b/apps/vpw_clock/ChangeLog new file mode 100644 index 000000000..7b5533a0f --- /dev/null +++ b/apps/vpw_clock/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: Now using a bigger font for day of week and date for better visibility +0.03: Set theme to light, add black as an option for foreground color +0.04: Handle fast loading +0.05: Fix theme reset for some themes +0.06: Minor fix: do not remove VGA8 font diff --git a/apps/vpw_clock/README.md b/apps/vpw_clock/README.md new file mode 100644 index 000000000..b0b2b79a8 --- /dev/null +++ b/apps/vpw_clock/README.md @@ -0,0 +1,21 @@ +# Vaporwave Sunset Clock +This is a simple clock with a Vaporwave Sunset theme. + +![Screenshot](screenshot.png) +![Screenshot 2](screenshot2.png) +![Screenshot 3](screenshot3.png) + +# Settings + +You can select the text color: +- white +- ref +- purple + +# Todo + +- add support for AM/PM time + +# Author + +paul-arg [github](https://github.com/paul-arg) \ No newline at end of file diff --git a/apps/vpw_clock/app-icon.js b/apps/vpw_clock/app-icon.js new file mode 100644 index 000000000..f90e4e6c6 --- /dev/null +++ b/apps/vpw_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcAtu27YC/AX4C/AX4C/AVnXroRO3oDBtwRMv9p02atPTCJf7AwgRK74gBEYWmpoRJ7wGF5oRJ8wjFzoRJ+nTps0AQYRI24jGzc2CJAgEAQQREBQc1EAYCDugWDEYe/EZm+/fvAR33799ARwj/EfruIARAj/AQ88+fPARwjRA")) diff --git a/apps/vpw_clock/app.js b/apps/vpw_clock/app.js new file mode 100644 index 000000000..606b35d2e --- /dev/null +++ b/apps/vpw_clock/app.js @@ -0,0 +1,178 @@ +Graphics.prototype.setFontMadeSunflower = function () { + // Actual height 46 (45 - 0) + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AAmAAwt/AwsP/AHF/4WFj/8AwkB//AB1I7Hg/wBws+O6s4AwsfFgp3Gg//AwkDIQpYH//gUQpQFn4qFNo0P/w4aj44FgKJGjiCOEwIuFAwI9En4GBKYZKBAAI3CDgQeECoQWDCoYWDv4GCOQUPBwZWBEgglCj/+D4SXBgKaCF4IOBeQc/GgMDLod/RQLqDgIOGg4OFgE8BwKjDgIEBn6aFgZ7DBwbeDDoROCFgcfNoUHLIRoHAwYZCBwiVQGgIACKwQlDIwYWCCoQWENgYtCWQIACDwIcDgFAAYUIAQMOO4aaCIwUAjACBjwOFgIpDVIUfCwUfBwJZEboiGEO4gOCO4YOCh6VLBxCzOYR4ADg53CAAZoCAAaGDAAaGCBxYAGBwcfZoQ7Ch/8JwSkCfYV/SohzCSofwCIKGECIN/NAfwg/nO4kA/gOFj+HBwMD8F/bYIOCngIBn0HBwWAAoIRBBwM4BAP8BwgnB4AODMwQOFK4IsDCoJLBHYZmBOAIOBN4J4BBwYGB/wOG4EPNAiWBcAuABwSGC+AODGQIzBj4OCv4zBGAKkDEgSzEwACCEoQVCBwTVCn5MBABZxBAwgzDHYPAAQI2BCQIDBHwLyBNAIOCKgIhDLgIDBBwJrDO4QKBDoKGCIwV/g4OCFALZBXAIODnkBGAIOBhgFBjgOCBYQOEnwXBBwYjBBwomCBwY1CBwfjKYQ7DvpPBg4OC9+f8EBPYJoB+JnBPYUfEoIGBd4fABwRoC/DiCBwaFCSofgQoKVDF4KjBKgUfwDBBGYUBCII0Bc4UeVYbYEZIYADh7nFgF8AwsPFYL2EdwQADfIQADj/cCov5Bwv/VQIVE4IOEg/4BwraDCobmCCofwBwMYCobXBgKkBgE/wAOECoIOBgYOBZofAvAVCWQTCCYATCFBwreCB3AACgPBdAQACNAYODQwgOCQwYOKgAOGdAsfTALhEIQr3Bf4cD/kH//gLIfAv//EIX//inBEoIODO4ngngdBO4X+gYdBg4ODvEHCIIOCBYM8KYQOCAoJiBBwZiDBwN8OIYOCBYIOMLwQODv4OBHYhPBEAQOC8EBP4IOCaIQOEcAgOENAc/AwKGBgZ3BBwQ2Bn/gS4KkCg/+S4X+BwIKBUYIzCh4KBGgIzBgACBEoIVCAAQWBdAovBAwg6DcwbTBfghCDfgZgDDgYWECoQWDCoZDDQoR3CJAQlFAwZhCEgYOCEgWAn40Cn/5GIM/NIMH/jOBgAOCv+DBYSOC8AOCCIcDfwkPwE+fwcA+EDJ4IOCjwxDBwMB8BEBwAgCBILgCfwXARwoOEfwWAcAS0CjBxDQwQQBSoqPDbIjvEVojZEEoLoFv4VEAAsfdgg5CGAgOJDoxVDBxQ/FgJkEBws/AYIODR4IOEPAKVCBwJwCJwIOCWYgOBToQODg4OGWYQODCoQODCoTRCBwIGCHYYVCJwUf8YbCNAaqFj/vSwiGBPAojBWZqkGXQoOBJoIOEVYoALFAkfgBxBEIUPQ4QwCIYPwJoLGD/+BPAIGBDQP8BIIlCBYPnSobOB/9/SoYGB+6kETYUPGgLdCBgMfPYIpBHIV8BwPwSwUDBwM8C4IOBjgJBwA1BGIIOBnEAXYQODCwOABwk/HIIODJgPgBwU8LYQODGIJfBHYU/wBMB+JZFAAIOBNAQABBwJ3CNQYOC/oGBJgKVCz6VF/AGBUgSaBT4QGBOwQJBYQUPJALRDKoQvBcAQACv4VCBgKcBAAbZBIAQACLwYACNwIWEOARICJQYyCd4glBjB3E4EBd4hQBXAIOEXAIzBwYOEVwQOBg4OBBIQOBFwIYCh/Ah5CBBwMAvkBJgIOCEAM/BYLgBAQMHP4LgCgfBNQQRDRwUDBQMH36kCn/+KgRHBcAiXCd4icER4iGCTwiVCVoakCXgizBEgiOEaIi7FCwL1DFoToFgECAQL+DgIVBv4eE8EPHgZ5CcQQlC+EfFwY8BIwJEDB0g7Hj72BOIhPBnxqFAAQ'))), + 46, + atob("DxMiFyAgIiAgICEgDw=="), + 60 | 65536 + ); +}; + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + require("FontVGA8").add(Graphics); + + const COLOUR_BLACK = 0x0; + const COLOUR_WHITE = 0xffff; + const COLOUR_RED = "#FF0000"; + const COLOUR_VPW_GREEN = 0xf0f; + const COLOUR_PURPLE = "#8000FF"; + //const COLOUR_DARK_GREY = "#3F3F3F"; + //const COLOUR_GREY = "#7F7F7F"; + //const COLOUR_LIGHT_GREY = "#BFBFBF"; + //const COLOUR_BLUE = "#0000FF"; + //const COLOUR_YELLOW = "#FFFF00"; + //const COLOUR_LIGHT_CYAN = "#7FFFFF"; + //const COLOUR_DARK_YELLOW = "#7F7F00"; + //const COLOUR_DARK_CYAN = "#007F7F"; + //const COLOUR_ORANGE = "#FF7F00"; + //const COLOUR_MAGENTA = "#ff00ff"; + + const sun_img = require("heatshrink").decompress(atob("2GwwZC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AUjdt23btuAH/MNHwQCD7CA8AQlsIGsGHwwCD2BAzgI+IAQfAIOVp22bARZAxjaAKAQdsIOGatOmARuAIF02QBgCEIFsBQBwCDQlsNQB4CC7BBsQCACDIFcDQCACDsBBqjSDUtBBq6dtmwCTIFMGQCQCD0BBognTps0AScwINEmQa2mIE8BmmTpICVwBBnQCoCCIM8NknSpoCV6BBmhKDYzRBmfyACJIMyAXQdECpMkyQCXoBBlQbVgIMkSQbVIIMkaQbVoQf6DmyVpkwCZIMqDapJB/IP5BnghB/IL0gIP5B/IP5B/IP5B/IP5B/IP5B/IP5B4gBB/ILxAjIP5B/IP4AGiRBapBB/IP5BogRBaoBB/IP4CCIEgABIP5B/AAcJILGQIM0BILGAIP5BogBBYIE8AghBWkBB/INUAIKxApgESIKlIINUCIKlAINUAIKhArgEJIKWQINkBIKWAINkAIKRAtAAJBQIF8AiRBOpBBwgBBOIGMAhJBMyBBygEEIJUgIGYABiRBIpBA1ZBLC0QxSA4AH4A/AH4A/AGA")); + + let settings = Object.assign({ + // default values + foregroundColor: 0 + }, require('Storage').readJSON("vpw_clock.settings.json", true) || {}); + + let foregroundColor; + + switch (settings.foregroundColor) { + case 0: + foregroundColor = COLOUR_RED; + break; + + case 1: + foregroundColor = COLOUR_PURPLE; + break; + + case 2: + foregroundColor = COLOUR_WHITE; + break; + + case 3: + foregroundColor = COLOUR_BLACK; + break; + + default: + foregroundColor = COLOUR_BLACK; // to detect problems + break; + } + + let drawPolygonWithGrid = function (x1, y1, x2, y2, x3, y3, x4, y4, M, N) { + // Draw the polygon + g.drawLine(x1, y1, x2, y2); + g.drawLine(x2, y2, x3, y3); + g.drawLine(x3, y3, x4, y4); + g.drawLine(x4, y4, x1, y1); + + for (let i = 1; i < N; i++) { + let xi1 = x1 + i * ((x2 - x1) / N); + let yi1 = y1 + i * ((y2 - y1) / N); + + let xi2 = x4 - i * ((x4 - x3) / N); + let yi2 = y4 - i * ((y4 - y3) / N); + + g.drawLine(xi1, yi1, xi2, yi2); + } + + for (let j = 1; j < M; j++) { + let xi1 = x1 + j * ((x4 - x1) / M); + let yi1 = y1 + j * ((y4 - y1) / M); + + let xi2 = x2 - j * ((x2 - x3) / M); + let yi2 = y2 - j * ((y2 - y3) / M); + + g.drawLine(xi1, yi1, xi2, yi2); + } + }; + + const SCREEN_WIDTH = 176; + const SCREEN_HEIGHT = 176; + const GROUND_HEIGHT = 176 - 45; + + const GRID_BASE_OFFSET = 100; + + // timeout used to update every minute + let drawTimeout; + + // schedule a draw for the next minute + let queueDraw = function () { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function () { + var x = g.getWidth() / 2; + var y = 24 + 20; + + g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()); + + //sky + g.setColor(COLOUR_VPW_GREEN); + g.fillRect(0, 24, SCREEN_WIDTH, GROUND_HEIGHT - 1); + + g.drawImage(sun_img, 0, 0); + + //ground + g.setColor("#8000FF"); + g.fillRect(0, GROUND_HEIGHT, 176, SCREEN_HEIGHT); + + //lines + g.setColor(COLOUR_WHITE); + drawPolygonWithGrid(0, GROUND_HEIGHT, + SCREEN_WIDTH, GROUND_HEIGHT, + SCREEN_WIDTH + GRID_BASE_OFFSET, SCREEN_HEIGHT - 1, + 0 - GRID_BASE_OFFSET, SCREEN_HEIGHT - 1, + 7, //vertical + 15); //horizontal + + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date, 1); + var dateStr = require("locale").date(date).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); + // draw time + g.setFontAlign(0, 0).setFontMadeSunflower().setColor(foregroundColor); + g.drawString(timeStr, x, y + 20); + // draw date + y += 35; + g.setFontAlign(0, 0, 1).setFont("VGA8"); + g.drawString(dateStr, g.getWidth() - 8, g.getHeight() / 2 - 10); + // draw the day of the week + g.setFontAlign(0, 0, 3).setFont("VGA8"); + g.drawString(dowStr, 8, g.getHeight() / 2 - 10); + // queue draw in one minute + queueDraw(); + }; + + // store the theme before drawing + let originalTheme = g.theme; + + // Clear the screen once, at startup + g.setTheme({ bg: COLOUR_VPW_GREEN, fg: foregroundColor, dark: false }).clear(); + + // draw immediately at first, queue update + draw(); + + // Show launcher when middle button pressed + // handle fast loading + Bangle.setUI({ + mode: "clock", + remove: function () { + // clear timeout + if (drawTimeout) clearTimeout(drawTimeout); + // remove custom font + delete Graphics.prototype.setFontMadeSunflower; + // revert theme to how it was before + g.setTheme(originalTheme); + } + }); + + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/vpw_clock/app.png b/apps/vpw_clock/app.png new file mode 100644 index 000000000..73c69d5a1 Binary files /dev/null and b/apps/vpw_clock/app.png differ diff --git a/apps/vpw_clock/metadata.json b/apps/vpw_clock/metadata.json new file mode 100644 index 000000000..87f53c71c --- /dev/null +++ b/apps/vpw_clock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "vpw_clock", + "name": "Vaporwave Sunset Clock", + "shortName": "Vaporwave Sunset", + "type": "clock", + "version":"0.06", + "description": "A clock with a vaporwave sunset theme.", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"vpw_clock.app.js","url":"app.js"}, + {"name":"vpw_clock.settings.js","url":"settings.js"}, + {"name":"vpw_clock.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"vpw_clock.settings.json"}], + "icon": "app.png", + "readme": "README.md", + "screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }, { "url": "screenshot3.png" }], + "allow_emulator":true +} diff --git a/apps/vpw_clock/screenshot.png b/apps/vpw_clock/screenshot.png new file mode 100644 index 000000000..a2ec0548c Binary files /dev/null and b/apps/vpw_clock/screenshot.png differ diff --git a/apps/vpw_clock/screenshot2.png b/apps/vpw_clock/screenshot2.png new file mode 100644 index 000000000..df9fc787f Binary files /dev/null and b/apps/vpw_clock/screenshot2.png differ diff --git a/apps/vpw_clock/screenshot3.png b/apps/vpw_clock/screenshot3.png new file mode 100644 index 000000000..c97f5257d Binary files /dev/null and b/apps/vpw_clock/screenshot3.png differ diff --git a/apps/vpw_clock/settings.js b/apps/vpw_clock/settings.js new file mode 100644 index 000000000..d8b17d995 --- /dev/null +++ b/apps/vpw_clock/settings.js @@ -0,0 +1,28 @@ +(function(back) { + var FILE = "vpw_clock.settings.json"; + // Load settings + var settings = Object.assign({ + foregroundColor: 0, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var foregroundColors = ["Red", "Purple", "White", "Black"]; + + // Show the menu + E.showMenu({ + "" : { "title" : "Vaporwave Sunset" }, + "< Back" : () => back(), + 'Foreground color': { + value: 0|settings.foregroundColor, // 0| converts undefined to 0 + min: 0, max: 3, + onchange: v => { + settings.foregroundColor = v; + writeSettings(); + }, + format: function (v) {return foregroundColors[v];} + }, + }); +}) \ No newline at end of file diff --git a/lang/eo.json b/lang/eo.json new file mode 100644 index 000000000..9e96e63fb --- /dev/null +++ b/lang/eo.json @@ -0,0 +1,241 @@ +{ + "//": "Esperanto translations", + "GLOBAL": { + "//": "Translations that apply for all apps", + "Alarms": "Alarmoj", + "Hours": "Horoj", + "Minutes": "Minutoj", + "Enabled": "Ebligita", + "Save": "Konservu", + "Back": "Reen", + "Repeat": "Ripeto", + "Delete": "Forigu", + "ALARM!": "ALARMO!", + "Sleep": "Dormi", + "New Alarm": "Nova alarmo", + "Yes": "Jes", + "No": "Ne", + "On": "Ŝaltita", + "Off": "Malŝaltita", + "Ok": "Bone", + "(repeat)": "(ripetu)", + "New Timer": "Nova tempumilo", + "music": "muziko", + "circle 2": "cirklo 2", + "circle 1": "cirklo 1", + "Keep Msgs": "Konservu Msĝj", + "circle 3": "cirklo 3", + "week": "semajno", + "Auto snooze": "Aŭtomata ripetado de alarmo", + "show widgets": "Malkovru kromprogrametoj", + "min. confidence": "minimuma fido", + "circle 4": "cirklo 4", + "circle count": "rekalkulado de cirkloj", + "heartrate": "kora kadenco", + "Heartrate": "Kora kadenco", + "weather circle": "meteologia cirklo", + "battery warn": "averto de baterio", + "minimum": "minimuma", + "distance goal": "celo de distanco", + "valid period": "tempo de valideco", + "maximum": "maksimuma", + "step length": "longo de paŝo", + "data": "datumoj", + "colorize icon": "kolorigo de bildeto", + "Circle": "Cirklo", + "Launcher Settings": "Agordo de lanĉilo", + "App Source\nNot found": "Fonto de apo\nNe trovita", + "Show clocks": "Montru horloĝojn", + "Font": "Fonto", + "TAP right top/bottom": "TAP dekstra supre/sube", + "Yes\ndefinitely": "Jes\nkompreneble", + "View Message": "Vidu mesaĝojn", + "Delete all messages": "Forigu ĉiujn mesaĝojn", + "STEPS": "PAŜOJ", + "BTNs 1:startlap 2:exit 3:reset": "BTNj 1:rondeko 2:eliri 3:restarigi", + "start&lap/reset, BTN1: EXIT": "eko&rondo/restarigo, BTN1: ELIRI", + "Are you sure": "Ĉu vi konsentas", + "Vector font size": "Mezuro de vektora tiparo", + "Mark Unread": "Marki kiel nelegita", + "No Messages": "Ne estas mesaĝoj", + "Delete All Messages": "Foriru ĉiujn mesaĝojn", + "LCD": "LKE", + "Apps": "Apoj", + "Unread timer": "Nelegita tempumilo", + "Record Run": "Rekordo de kurado", + "Bluetooth": "Bludento", + "Quiet Mode": "Silenta reĝimo", + "Piezo": "Piezo", + "Make Connectable": "Konektebligi", + "Programmable": "Programebla", + "Vibration": "Vibro", + "Passkey BETA": "Pasa ŝlosilo BETA", + "Customize": "Tajlori", + "HID": "HID", + "Utils": "Utilaĵoj", + "Light BW": "Hela fono", + "BLE": "BLE", + "Dark BW": "Malhela fono", + "Background 2": "2-a fono", + "Foreground 2": "2-a malfono", + "Foreground": "Malfono", + "Highlight BG": "Prilumi fonon", + "Connect device\nto add to\nwhitelist": "Konekti ilon\npor meti ĝin\nen blankan liston", + "Highlight FG": "Prilumi malfonon", + "Background": "Fono", + "Add Device": "Aldoni ilon", + "Remove": "Forigi", + "Wake on BTN3": "Vekigi per BTN3", + "Twist Max Y": "Max Y giro", + "LCD Timeout": "Tempolimo de LKE", + "Twist Threshold": "Gir-limo", + "Wake on BTN2": "Vekigu per BTN2", + "Wake on BTN1": "Vekigu per BTN1", + "Wake on Twist": "Vekigu per giro", + "LCD Brightness": "Brileco de LKE", + "Log": "Registro", + "Time Zone": "Horzono", + "Wake on FaceUp": "Vekigu kiam frontsupra", + "Wake on Touch": "Vekigu per tuŝado", + "Twist Timeout": "Tempolimo de giro", + "Compact Storage": "Kompaktigi konservejon", + "Clock Style": "Stilo de horloĝo", + "Storage": "Konservejo", + "Utilities": "Iloj", + "Compacting...\nTakes approx\n1 minute": "Kompaktigante...\nTio malfruas pli-malpli\n1 minuton", + "Debug Info": "Informo de la senerarigo", + "Rewrite Settings": "Restarigi agordojn", + "Flatten Battery": "Malŝargi baterion", + "Turn Off": "Malŝalti", + "This will remove everything": "Tio forigos ĉion", + "Reset Settings": "Restarigi agordojn", + "Month": "Monato", + "Second": "Sekundo", + "Date": "Dato", + "Reset to Defaults": "Restarigi al la defaŭltaj valoroj", + "Hour": "Horo", + "Flattening battery - this can take hours.\nLong-press button to cancel": "Malŝargante baterion - eble malfruos horojn.\nTenu butonon premita por nuligi", + "Stay Connectable": "Tenu konektita", + "Minute": "Minuto", + "No Clocks Found": "Neniu horloĝo trovita", + "Connectable": "Konektebla", + "No app has settings": "Neniu apo havas agordojn", + "Invalid settings": "Nevalidaj agordoj", + "App Settings": "Agordoj de apo", + "Side": "Flanko", + "OFF": "MALŜALTITA", + "Sleep Phase Alarm": "Averto de fazo de dormado", + "Widgets": "Kromprogrametoj", + "Left": "Maldekstra", + "Sort Order": "Ordiga ordo", + "TIMER": "TEMPUMILO", + "goal": "celo", + "Right": "Dekstra", + "on": "en", + "Alarm": "Averto", + "Reset All": "Restarigi ĉion", + "Reset all widgets": "Restarigi ĉiujn kromprogrametojn", + "Reset": "Restarigi", + "Beep": "Sono", + "System": "Sistemo", + "Locale": "Locaĵaro", + "Message": "Mesaĝo", + "Set Time": "Starigi tempon", + "Vibrate": "Vibrado", + "Alerts": "Avertoj", + "Timer": "Tempumilo", + "Error in settings": "Eraro en agordoj", + "Select Clock": "Elektu horloĝon", + "Whitelist": "Blanka listo", + "Disable": "Malaktivigi", + "BACK": "REEN", + "Factory Reset": "Fabrik-agordoj", + "Connected": "Konektita", + "ALARM": "AVERTO", + "Messages": "Mesaĝoj", + "Settings": "Agordoj", + "Show": "Montru", + "Hide": "Kovru", + "steps": "paŝoj", + "back": "reen", + "Steps": "Paŝoj", + "Year": "Jaro", + "Loading": "Ŝutadante", + "Music": "Muziko", + "color": "koloro", + "off": "malŝaltita", + "Theme": "Temo", + "one": "unu", + "two": "du", + "three": "tri", + "four": "kvar", + "five": "kvin", + "six": "ses", + "seven": "sep", + "eight": "ok", + "nine": "naŭ", + "ten": "dek", + "eleven": "dek unu", + "twelve": "dek du", + "Clock": "Horloĝo", + "Day": "Tago", + "Date & Time": "Tago kaj dato", + "About": "Pri", + "Enable All": "Aktivigi ĉion", + "Disable All": "Malaktivigi ĉion", + "Delete All": "Foriru ĉion", + "Yes": "Jes", + "No": "Ne", + "Alarms & Timers": "Avertoj kaj tempumiloj", + "Advanced" : "Altnivela", + "Time Format": "Tempoformo", + "Start Week On": "Semajno komenciĝas en", + "Launcher": "Lanĉilo", + "Calibrate Battery": "Kalibri baterion", + "New": "Nova", + "No clocks found": "Neniu horloĝo trovita", + "Confirm": "Konfirmi", + "Cancel": "Nuligi", + "Week": "Semajno", + "Event": "Evento", + "New Event": "Nova evento", + "Edit Event": "Redakti eventon", + "Edit Alarm": "Redakti averton", + "Health": "Sano", + "Step Counting": "paŝ-kalkulado", + "distance": "distanco", + "Distance": "Distanco", + "HRM Interval": "Intervalo KKM", + "3 min": "3 min", + "10 min": "10 min", + "Always": "Ĉiam", + "Daily Step Goal": "Ĉiutaga celo de paŝoj", + "HRM Record": "Registro KKM", + "per hour": "laŭhore", + "per day": "laŭtage", + "Movement": "Movado", + "Heart Rate": "Kora kadenco", + "Step Goal Notification": "Sciigo de celo de paŝoj", + "HOUR": "HORO", + "DAY": "TAGO" + }, + "alarm": { + "//": "App-specific overrides", + "rpt": "rip." + }, + "fuzzyw": { + "//": "App-specific overrides", + "*$1 o'clock": "*$1 ĝuste", + "five past *$1": "*$1 kaj kvin", + "ten past *$1": "*$1 kaj dek", + "quarter past *$1": "*$1 kaj kvarono", + "twenty past *$1": "*$1 kaj dudek", + "twenty five past *$1": "*$1 kaj dudek kvin", + "half past *$1": "*$1 kaj duono", + "twenty five to *$2": "*$1 kaj tridek-kvin", + "twenty to *$2": "*$1 kaj kvardek", + "quarter to *$2": "*$1 kaj kvardek-kvin", + "ten to *$2": "*$1 kaj kvindek", + "five to *$2": "*$1 kaj kvindek-kvin" + } +} diff --git a/modules/.eslintrc.js b/modules/.eslintrc.js new file mode 100644 index 000000000..cc0dd660e --- /dev/null +++ b/modules/.eslintrc.js @@ -0,0 +1,181 @@ +module.exports = { + "env": { + // TODO: "espruino": false + // TODO: "banglejs": false + // For a prototype of the above, see https://github.com/espruino/BangleApps/pull/3237 + }, + "extends": "eslint:recommended", + "globals": { + // Methods and Fields at https://banglejs.com/reference + "Array": "readonly", + "ArrayBuffer": "readonly", + "ArrayBufferView": "readonly", + "Bangle": "readonly", + "BluetoothDevice": "readonly", + "BluetoothRemoteGATTCharacteristic": "readonly", + "BluetoothRemoteGATTServer": "readonly", + "BluetoothRemoteGATTService": "readonly", + "Boolean": "readonly", + "console": "readonly", + "DataView": "readonly", + "Date": "readonly", + "E": "readonly", + "Error": "readonly", + "Flash": "readonly", + "Float32Array": "readonly", + "Float64Array": "readonly", + "fs": "readonly", + "Function": "readonly", + "Graphics": "readonly", + "heatshrink": "readonly", + "I2C": "readonly", + "Int16Array": "readonly", + "Int32Array": "readonly", + "Int8Array": "readonly", + "InternalError": "readonly", + "JSON": "readonly", + "Math": "readonly", + "Modules": "readonly", + "NRF": "readonly", + "Number": "readonly", + "Object": "readonly", + "OneWire": "readonly", + "Pin": "readonly", + "process": "readonly", + "Promise": "readonly", + "ReferenceError": "readonly", + "RegExp": "readonly", + "Serial": "readonly", + "SPI": "readonly", + "Storage": "readonly", + "StorageFile": "readonly", + "String": "readonly", + "SyntaxError": "readonly", + "tensorflow": "readonly", + "TFMicroInterpreter": "readonly", + "TypeError": "readonly", + "Uint16Array": "readonly", + "Uint24Array": "readonly", + "Uint32Array": "readonly", + "Uint8Array": "readonly", + "Uint8ClampedArray": "readonly", + "Unistroke": "readonly", + "Waveform": "readonly", + // Methods and Fields at https://banglejs.com/reference + "__FILE__": "readonly", + "analogRead": "readonly", + "analogWrite": "readonly", + "arguments": "readonly", + "atob": "readonly", + "Bluetooth": "readonly", + "BTN": "readonly", + "BTN1": "readonly", + "BTN2": "readonly", + "BTN3": "readonly", + "BTN4": "readonly", + "BTN5": "readonly", + "btoa": "readonly", + "changeInterval": "readonly", + "clearInterval": "readonly", + "clearTimeout": "readonly", + "clearWatch": "readonly", + "decodeURIComponent": "readonly", + "digitalPulse": "readonly", + "digitalRead": "readonly", + "digitalWrite": "readonly", + "dump": "readonly", + "echo": "readonly", + "edit": "readonly", + "encodeURIComponent": "readonly", + "eval": "readonly", + "getPinMode": "readonly", + "getSerial": "readonly", + "getTime": "readonly", + "global": "readonly", + "globalThis": "writable", + "HIGH": "readonly", + "I2C1": "readonly", + "Infinity": "readonly", + "isFinite": "readonly", + "isNaN": "readonly", + "LED": "readonly", + "LED1": "readonly", + "LED2": "readonly", + "load": "readonly", + "LoopbackA": "readonly", + "LoopbackB": "readonly", + "LOW": "readonly", + "NaN": "readonly", + "parseFloat": "readonly", + "parseInt": "readonly", + "peek16": "readonly", + "peek32": "readonly", + "peek8": "readonly", + "pinMode": "readonly", + "poke16": "readonly", + "poke32": "readonly", + "poke8": "readonly", + "print": "readonly", + "require": "readonly", + "reset": "readonly", + "save": "readonly", + "Serial1": "readonly", + "setBusyIndicator": "readonly", + "setInterval": "readonly", + "setSleepIndicator": "readonly", + "setTime": "readonly", + "setTimeout": "readonly", + "setWatch": "readonly", + "shiftOut": "readonly", + "SPI1": "readonly", + "Terminal": "readonly", + "trace": "readonly", + "VIBRATE": "readonly", + // Aliases and not defined at https://banglejs.com/reference + "g": "readonly", + "WIDGETS": "readonly", + "module": "readonly", + "exports": "writable", + "D0": "readonly", + "D1": "readonly", + "D2": "readonly", + "D3": "readonly", + "D4": "readonly", + "D5": "readonly", + "D6": "readonly", + "D7": "readonly", + "D8": "readonly", + "D9": "readonly", + "D10": "readonly", + "D11": "readonly", + "D12": "readonly", + "D13": "readonly", + "D14": "readonly", + "D15": "readonly", + "D16": "readonly", + "D17": "readonly", + "D18": "readonly", + "D19": "readonly", + "D20": "readonly", + "D21": "readonly", + "D22": "readonly", + "D23": "readonly", + "D24": "readonly", + "D25": "readonly", + "D26": "readonly", + "D27": "readonly", + "D28": "readonly", + "D29": "readonly", + "D30": "readonly", + "D31": "readonly", + }, + "parserOptions": { + "ecmaVersion": 11, + }, + "rules": { + "no-delete-var": "off", + "no-global-assign": "off", + "no-inner-declarations": "off", // TODO: remove this after upgrade to ESLint 9 + "no-control-regex": "off", + }, +} diff --git a/modules/ble_advert.js b/modules/ble_advert.js new file mode 100644 index 000000000..0a037cfd8 --- /dev/null +++ b/modules/ble_advert.js @@ -0,0 +1,69 @@ +var advertise = function (options) { + var clone = function (obj) { + if (Array.isArray(obj)) { + return obj.map(clone); + } + else if (typeof obj === "object") { + var r = {}; + for (var k in obj) { + r[k] = clone(obj[k]); + } + return r; + } + return obj; + }; + NRF.setAdvertising(clone(Bangle.bleAdvert), options); +}; +var manyAdv = function (bleAdvert) { + return Array.isArray(bleAdvert) && typeof bleAdvert[0] === "object"; +}; +exports.set = function (id, advert, options) { + var _a, _b, _c; + var bangle = Bangle; + if (manyAdv(bangle.bleAdvert)) { + var found = false; + var obj = void 0; + for (var _i = 0, _d = bangle.bleAdvert; _i < _d.length; _i++) { + var ad = _d[_i]; + if (Array.isArray(ad)) + continue; + obj = ad; + if (ad[id]) { + ad[id] = advert; + found = true; + break; + } + } + if (!found) { + if (obj) + obj[id] = advert; + else + bangle.bleAdvert.push((_a = {}, _a[id] = advert, _a)); + } + } + else if (bangle.bleAdvert) { + if (Array.isArray(bangle.bleAdvert)) { + bangle.bleAdvert = [bangle.bleAdvert, (_b = {}, _b[id] = advert, _b)]; + } + else { + bangle.bleAdvert[id] = advert; + } + } + else { + bangle.bleAdvert = (_c = {}, _c[id] = advert, _c); + } + advertise(options); +}; +exports.push = function (adv, options) { + var bangle = Bangle; + if (manyAdv(bangle.bleAdvert)) { + bangle.bleAdvert.push(adv); + } + else if (bangle.bleAdvert) { + bangle.bleAdvert = [bangle.bleAdvert, adv]; + } + else { + bangle.bleAdvert = [adv, {}]; + } + advertise(options); +}; diff --git a/modules/ble_advert.ts b/modules/ble_advert.ts new file mode 100644 index 000000000..c0b852f1d --- /dev/null +++ b/modules/ble_advert.ts @@ -0,0 +1,136 @@ +declare let exports: any; +//declare let BLE_DEBUG: undefined | true; + +type BleAdvertObj = { [key: string | number]: number[] }; +type BleAdvert = BleAdvertObj | number[]; +type BangleWithAdvert = (typeof Bangle) & { bleAdvert?: BleAdvert | BleAdvert[]; }; +type SetAdvertisingOptions = typeof NRF.setAdvertising extends (data: any, options: infer Opts) => any ? Opts : never; + +const advertise = (options: SetAdvertisingOptions) => { + const clone = (obj: any): any => { + // just for our use-case + if(Array.isArray(obj)){ + return obj.map(clone); + }else if(typeof obj === "object"){ + const r = {}; + for(const k in obj){ + // @ts-expect-error implicitly + r[k] = clone(obj[k]); + } + return r; + } + return obj; + }; + + // clone the object, to avoid firmware behaving like so: + // bleAdvert = [Uint8Array, { [0x180f]: ... }] + // ^ ^ + // | | + // | +- added by this call + // +- modified from a previous setAdvertising() + // + // The firmware (jswrap_ble_setAdvertising) will convert arrays within + // the advertising array to Uint8Array, but if this has already been done, + // we get iterator errors. So we clone the object to avoid these mutations + // taking effect for later calls. + // + // This also allows us to identify previous adverts correctly by id. + NRF.setAdvertising(clone((Bangle as BangleWithAdvert).bleAdvert), options); +}; + +const manyAdv = (bleAdvert: BleAdvert | BleAdvert[] | undefined): bleAdvert is BleAdvert[] => { + return Array.isArray(bleAdvert) && typeof bleAdvert[0] === "object"; +}; + +exports.set = (id: string | number, advert: number[], options?: SetAdvertisingOptions) => { + const bangle = Bangle as BangleWithAdvert; + + if(manyAdv(bangle.bleAdvert)){ + let found = false; + let obj; + for(let ad of bangle.bleAdvert){ + if(Array.isArray(ad)) continue; + obj = ad; + if(ad[id]){ + ad[id] = advert; + found = true; + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`bleAdvert is array, found existing entry for ${id}, replaced`) + break; + } + } + if(!found){ + if(obj) + obj[id] = advert; + else + bangle.bleAdvert.push({ [id]: advert }); + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`bleAdvert is array, no entry for ${id}, created`) + } + }else if(bangle.bleAdvert){ + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`bleAdvert is object, ${id} entry ${id in bangle.bleAdvert ? "replaced" : "created"}`); + if(Array.isArray(bangle.bleAdvert)){ + bangle.bleAdvert = [bangle.bleAdvert, { [id]: advert }]; + }else{ + bangle.bleAdvert[id] = advert; + } + }else{ + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`bleAdvert not present, created`); + bangle.bleAdvert = { [id]: advert }; + } + + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`NRF.setAdvertising({ ${Object.keys(bangle.bleAdvert).join(", ")} }, ${JSON.stringify(options)})`); + + advertise(options); +}; + +exports.push = (adv: number[], options?: SetAdvertisingOptions) => { + const bangle = Bangle as BangleWithAdvert; + + if(manyAdv(bangle.bleAdvert)){ + bangle.bleAdvert.push(adv); + }else if(bangle.bleAdvert){ + bangle.bleAdvert = [bangle.bleAdvert, adv]; + }else{ + // keep a second entry for normal/original advertising as well as this extra one + bangle.bleAdvert = [adv, {}]; + } + + advertise(options); +}; + +/* +exports.remove = (id: string | number, options?: SetAdvertisingOptions) => { + const bangle = Bangle as BangleWithAdvert; + + // if(typeof BLE_DEBUG !== "undefined") + // console.log(`ble_advert.remove(${id}, ${JSON.stringify(options)})`); + + if(manyAdv(bangle.bleAdvert)){ + let i = 0; + for(const ad of bangle.bleAdvert){ + if(Array.isArray(ad)) continue; + if(ad[id]){ + delete ad[id]; + let empty = true; + // eslint-disable-next-line no-unused-vars + for(const _ in ad){ + empty = false; + break; + } + if(empty) bangle.bleAdvert.splice(i, 1); + break; + } + i++; + } + }else if(bangle.bleAdvert){ + if(!Array.isArray(bangle.bleAdvert)) + delete bangle.bleAdvert[id]; + } + + advertise(options); +}; +*/ diff --git a/modules/more_pickers.js b/modules/more_pickers.js new file mode 100644 index 000000000..596b36fdf --- /dev/null +++ b/modules/more_pickers.js @@ -0,0 +1,198 @@ +/* see more_pickers.md for more information */ + +exports.doublePicker = function (options) { + var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF"; + + var R = Bangle.appRect; + g.reset().clearRect(R); + g.setFont("12x20").setFontAlign(0, 0).drawString(menuIcon + " " + options.title, R.x + R.w / 2, R.y + 12); + + var v_1 = options.value_1; + var v_2 = options.value_2; + + function draw() { + g.setColor(g.theme.bg2) + .fillRect(14, 60, 81, 166) + .fillRect(95, 60, 162, 166); + + g.setColor(g.theme.fg2) + .fillPoly([47.5, 68, 62.5, 83, 32.5, 83]) + .fillPoly([47.5, 158, 62.5, 143, 32.5, 143]) + .fillPoly([128.5, 68, 143.5, 83, 113.5, 83]) + .fillPoly([128.5, 158, 143.5, 143, 113.5, 143]); + + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; + + g.setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 47.5, 113) + .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_2))) + .drawString(txt_2, 128.5, 113) + .setFontVector(30) + .drawString(options.separator ?? "", 88, 110); + } + function cb(dir, x_part) { + if (dir) { + if (x_part == -1) { + v_1 -= (dir || 1) * (options.step_1 || 1); + if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; + if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + } else { + v_2 -= (dir || 1) * (options.step_2 || 1); + if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_2; + if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_2; + } + draw(); + } else { // actually selected + options.value_1 = v_1; + options.value_2 = v_2; + if (options.onchange) options.onchange(options.value_1, options.value_2); + options.back(); // redraw original menu + } + } + + draw(); + var dy = 0; + + Bangle.setUI({ + mode: "custom", + back: options.back, + remove: options.remove, + redraw: draw, + drag: e => { + dy += e.dy; // after a certain amount of dragging up/down fire cb + if (!e.b) dy = 0; + var x_part; + if (e.x <= 88) { + x_part = -1; + } else { + x_part = 1; + } + while (Math.abs(dy) > 32) { + if (dy > 0) { dy -= 32; cb(1, x_part); } + else { dy += 32; cb(-1, x_part); } + Bangle.buzz(20); + } + }, + touch: (_, e) => { + Bangle.buzz(20); + var x_part; + if (e.x <= 88) { + x_part = -1; + } else { + x_part = 1; + } + if (e.y < 82) cb(-1, x_part); // top third + else if (e.y > 142) cb(1, x_part); // bottom third + else cb(); // middle = accept + } + }); +} + +exports.triplePicker = function (options) { + var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF"; + + var R = Bangle.appRect; + g.reset().clearRect(R); + g.setFont("12x20").setFontAlign(0, 0).drawString(menuIcon + " " + options.title, R.x + R.w / 2, R.y + 12); + + var v_1 = options.value_1; + var v_2 = options.value_2; + var v_3 = options.value_3; + + function draw() { + g.setColor(g.theme.bg2) + .fillRect(8, 60, 56, 166) + .fillRect(64, 60, 112, 166) + .fillRect(120, 60, 168, 166); + + g.setColor(g.theme.fg2) + .fillPoly([32, 68, 47, 83, 17, 83]) + .fillPoly([32, 158, 47, 143, 17, 143]) + .fillPoly([88, 68, 103, 83, 73, 83]) + .fillPoly([88, 158, 103, 143, 73, 143]) + .fillPoly([144, 68, 159, 83, 129, 83]) + .fillPoly([144, 158, 159, 143, 129, 143]); + + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; + var txt_3 = options.format_3 ? options.format_3(v_3) : v_3; + + g.setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 32, 113) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_2))) + .drawString(txt_2, 88, 113) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_3))) + .drawString(txt_3, 144, 113) + .setFontVector(30) + .drawString(options.separator_1 ?? "", 60, 113) + .drawString(options.separator_2 ?? "", 116, 113); + } + function cb(dir, x_part) { + if (dir) { + if (x_part == -1) { + v_1 -= (dir || 1) * (options.step_1 || 1); + if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; + if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + } else if (x_part == 0) { + v_2 -= (dir || 1) * (options.step_2 || 1); + if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_3; + if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_3; + } else { + v_3 -= (dir || 1) * (options.step_3 || 1); + if (options.min_3 !== undefined && v_3 < options.min_3) v_3 = options.wrap_3 ? options.max_3 : options.min_3; + if (options.max_3 !== undefined && v_3 > options.max_3) v_3 = options.wrap_3 ? options.min_3 : options.max_3; + } + draw(); + } else { // actually selected + options.value_1 = v_1; + options.value_2 = v_2; + options.value_3 = v_3; + if (options.onchange) options.onchange(options.value_1, options.value_2, options.value_3); + options.back(); // redraw original menu + } + } + + draw(); + var dy = 0; + + Bangle.setUI({ + mode: "custom", + back: options.back, + remove: options.remove, + redraw: draw, + drag: e => { + dy += e.dy; // after a certain amount of dragging up/down fire cb + if (!e.b) dy = 0; + var x_part; + if (e.x <= 58) { + x_part = -1; + } else if (58 < e.x && e.x <= 116) { + x_part = 0; + } else { + x_part = 1; + } + while (Math.abs(dy) > 32) { + if (dy > 0) { dy -= 32; cb(1, x_part); } + else { dy += 32; cb(-1, x_part); } + Bangle.buzz(20); + } + }, + touch: (_, e) => { + Bangle.buzz(20); + var x_part; + if (e.x <= 58) { + x_part = -1; + } else if (58 < e.x && e.x <= 116) { + x_part = 0; + } else { + x_part = 1; + } + if (e.y < 82) cb(-1, x_part); // top third + else if (e.y > 142) cb(1, x_part); // bottom third + else cb(); // middle = accept + } + }); +} \ No newline at end of file diff --git a/modules/more_pickers.md b/modules/more_pickers.md new file mode 100644 index 000000000..1d37af44e --- /dev/null +++ b/modules/more_pickers.md @@ -0,0 +1,93 @@ +# More pickers + +This library provides a double picker and a triple picker, similar to the stock picker. + +# How to use +**Important:** you need to define a `back` handler that will be called to go back to the previous screen when the user confirms the input or clicks on the back button. + +It is possible to define an optionnal custom separator between the values. See examples below. + +## Double picker + +Example: + +```javascript +// example of a formatting function +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +var hours = 10; +var minutes = 32; + +function showMainMenu() { + E.showMenu({ + 'Time': function () { + require("more_pickers").doublePicker({ + back: showMainMenu, + title: "Time", + separator: ":", + + value_1: hours, + min_1: 0, max_1: 23, step_1: 1, wrap_1: true, + + value_2: minutes, + min_2: 0, max_2: 59, step_2: 1, wrap_2: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2) { hours = v_1; minutes = v_2; } + }); + } + }); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +showMainMenu(); +``` + + +## Triple picker + +Example: + +```javascript +// example of a formatting function +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +var day = 21; +var month = 5; +var year = 2021; + +function showMainMenu() { + E.showMenu({ + 'Date': function () { + require("more_pickers").triplePicker({ + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: day, + min_1: 1, max_1: 31, step_1: 1, wrap_1: true, + + value_2: month, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: year, + min_3: 2000, max_3: 2050, step_3: 1, wrap_3: false, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { day = v_1; month = v_2; year = v_3; } + }); + } + }); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +showMainMenu(); \ No newline at end of file