diff --git a/.eslintignore b/.eslintignore index f28e67b54..1e3abd9ff 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,11 +1,6 @@ -apps/animclk/V29.LBM.js -apps/banglerun/rollup.config.js -apps/schoolCalendar/fullcalendar/main.js -apps/authentiwatch/qr_packed.js -apps/qrcode/qr-scanner.umd.min.js -apps/gipy/pkg/gps.js -apps/health/chart.min.js -*.test.js -# typescript/generated files -apps/btadv/*.js +# Needs to be ignored because it uses ESM export/import +apps/gipy/pkg/gps.js + +# Needs to be ignored because it includes broken JS +apps/health/chart.min.js diff --git a/modules/.eslintrc.json b/.eslintrc.js similarity index 59% rename from modules/.eslintrc.json rename to .eslintrc.js index d656c2555..e79f87a5d 100644 --- a/modules/.eslintrc.json +++ b/.eslintrc.js @@ -1,7 +1,32 @@ -{ +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": { @@ -23,10 +48,8 @@ "Flash": "readonly", "Float32Array": "readonly", "Float64Array": "readonly", - "fs": "readonly", "Function": "readonly", "Graphics": "readonly", - "heatshrink": "readonly", "I2C": "readonly", "Int16Array": "readonly", "Int32Array": "readonly", @@ -46,11 +69,9 @@ "RegExp": "readonly", "Serial": "readonly", "SPI": "readonly", - "Storage": "readonly", "StorageFile": "readonly", "String": "readonly", "SyntaxError": "readonly", - "tensorflow": "readonly", "TFMicroInterpreter": "readonly", "TypeError": "readonly", "Uint16Array": "readonly", @@ -58,8 +79,10 @@ "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", @@ -129,7 +152,43 @@ "VIBRATE": "readonly", // Aliases and not defined at https://banglejs.com/reference "g": "readonly", - "WIDGETS": "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 @@ -142,22 +201,49 @@ "SwitchCase": 1 } ], - "no-case-declarations": "off", "no-constant-condition": "off", "no-delete-var": "off", - "no-empty": "off", + "no-empty": ["warn", { "allowEmptyCatch": true }], "no-global-assign": "off", "no-inner-declarations": "off", - "no-octal": "off", "no-prototype-builtins": "off", "no-redeclare": "off", "no-unreachable": "warn", "no-cond-assign": "warn", "no-useless-catch": "warn", - // TODO: "no-undef": "warn", - "no-undef": "off", - "no-unused-vars": "off", + "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/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7c0cfca3a..bebe18748 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -11,10 +11,10 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive - - name: Use Node.js 16.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x - name: Install testing dependencies run: npm ci - name: Test all apps and widgets 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/README.md b/README.md index ed6a501ef..d595c7df1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) +The release version is manually refreshed with regular intervals while the development version is continuously updated as new code is committed to this repository. + **All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, and that it is not licensed in another way that would make this impossible. @@ -251,7 +253,7 @@ and which gives information about the app for the Launcher. "description": "...", // long description (can contain markdown) "icon": "icon.png", // icon in apps/ "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app - "type":"...", // optional(if app) - + "type":"...", // optional(if app) - // 'app' - an application // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget @@ -300,7 +302,7 @@ and which gives information about the app for the Launcher. "customConnect": true, // if supplied, ensure we are connected to a device // before the "custom.html" iframe is loaded. An // onInit function in "custom.html" is then called - // with info on the currently connected device. + // with info on the currently connected device. "interface": "interface.html", // if supplied, apps/interface.html is loaded in an // iframe, and it may interact with the connected Bangle @@ -328,9 +330,9 @@ and which gives information about the app for the Launcher. {"name":"appid.data.json", // filename used in storage "storageFile":true // if supplied, file is treated as storageFile "url":"", // if supplied URL of file to load (currently relative to apps/) - "content":"...", // if supplied, this content is loaded directly + "content":"...", // if supplied, this content is loaded directly "evaluate":true, // if supplied, data isn't quoted into a String before upload - // (eg it's evaluated as JS) + // (eg it's evaluated as JS) }, {"wildcard":"appid.data.*" // wildcard of filenames used in storage }, // this is mutually exclusive with using "name" @@ -403,7 +405,7 @@ in an iframe. - +
Loading...
+ + + + + \ 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/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 5657bf26d..6780313ce 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -5,4 +5,6 @@ 0.05: Improved colors (connected vs disconnected) 0.06: Tell clock widgets to hide. 0.07: Convert Yes/No On/Off in settings to checkboxes -0.08: Fixed typo in settings.js for DRAGDOWN to make option work \ No newline at end of file +0.08: Fixed typo in settings.js for DRAGDOWN to make option work +0.09: You can now back out of the calendar using the button +0.10: Fix linter warnings diff --git a/apps/clockcal/README.md b/apps/clockcal/README.md index d30205be0..bc05081ad 100644 --- a/apps/clockcal/README.md +++ b/apps/clockcal/README.md @@ -7,23 +7,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu |:--:|:-| |![locked screen](screenshot.png)|locked: triggers only one minimal update/min| |![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds| -|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)| +|![big calendar](screenshot3.png)|swipe up for big calendar
⬆️/⬇️ to scroll
⬅️/➡️ to exit| ## Configurable Features - Number of calendar rows (weeks) -- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) +- Buzz on connect/disconnect (feel free to disable and use a widget) - Clock Mode (24h/12h). (No am/pm indicator) - First day of the week - Red Saturday/Sunday - Swipe/Drag gestures to launch features or apps. -## Auto detects your message/music apps: -- swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable) -- swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable) +## Integrated swipe launcher: (Configure in Settings) +- ⬇️ (down) will search your files for an app with the string "**message**" +- ➡️ (right) will search your files for an app with the string "**music**" +- ⬅️ (left) will search your files for an app with the string "**agenda**" +- ⬆️ (up) will show the **internal full calendar** ## Feedback -The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. -So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues +If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues (I moved my github repo) ## Planned features: - Internal lightweight music control, because switching apps has a loading time. diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js index 58ddd7ef5..185f2adea 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -24,15 +24,25 @@ const DEBUG = false; var state = "watch"; var monthOffset = 0; +// FIXME: These variables should maybe be defined inside relevant functions below. The linter complained they were not defined (i.e. they were added to global scope if I understand correctly). +let dayInterval; +let secondInterval; +let minuteInterval; +let newmonth; +let bottomrightY; +let bottomrightX; +let rMonth; +let dimSeconds; + /* * Calendar features */ function drawFullCalendar(monthOffset) { - addMonths = function (_d, _am) { - var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); + const addMonths = function (_d, _am) { + let ay = 0, m = _d.getMonth(), y = _d.getFullYear(); while ((m + _am) > 11) { ay++; _am -= 12; } while ((m + _am) < 0) { ay--; _am += 12; } - n = new Date(_d.getTime()); + let n = new Date(_d.getTime()); n.setMonth(m + _am); n.setFullYear(y + ay); return n; @@ -45,10 +55,10 @@ function drawFullCalendar(monthOffset) { if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); - d = addMonths(Date(), monthOffset); - tdy = Date().getDate() + "." + Date().getMonth(); + var d = addMonths(Date(), monthOffset); + let tdy = Date().getDate() + "." + Date().getMonth(); newmonth = false; - c_y = 0; + let c_y = 0; g.reset(); g.setBgColor(0); g.clear(); @@ -60,8 +70,8 @@ function drawFullCalendar(monthOffset) { rD.setDate(rD.getDate() - dow); var rDate = rD.getDate(); bottomrightY = c_y - 3; - clrsun = s.REDSUN ? '#f00' : '#fff'; - clrsat = s.REDSUN ? '#f00' : '#fff'; + let clrsun = s.REDSUN ? '#f00' : '#fff'; + let clrsat = s.REDSUN ? '#f00' : '#fff'; var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat]; for (var y = 1; y <= 11; y++) { bottomrightY += CELL_H; @@ -90,7 +100,7 @@ function caldrawMonth(rDate, c, m, rD) { g.setColor(c); g.setFont("Vector", 18); g.setFontAlign(-1, 1, 1); - drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; + let drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1); newmonth = false; } @@ -124,7 +134,7 @@ function drawMinutes() { var d = new Date(); var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' '); var minutes = d.getMinutes().toString().padStart(2, '0'); - var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff'; + var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00'; var size = 50; var clock_x = (w - 20) / 2; if (dimSeconds) { @@ -156,7 +166,7 @@ function drawSeconds() { } function drawWatch() { - if (DEBUG) console.log("CALENDAR"); + if (DEBUG) console.log("DRAWWATCH"); monthOffset = 0; state = "watch"; var d = new Date(); @@ -197,6 +207,7 @@ function drawWatch() { if (DEBUG) console.log("Next Day:" + (nextday / 3600)); if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); dayInterval = setTimeout(drawWatch, nextday * 1000); + if (DEBUG) console.log("ended DRAWWATCH. next refresh in " + nextday + "s"); } function BTevent() { @@ -211,8 +222,12 @@ function action(a) { g.reset(); if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); if (DEBUG) console.log("action:" + a); + state = "unknown"; + console.log("state -> unknown"); + let l; switch (a) { case "[ignore]": + drawWatch(); break; case "[calend.]": drawFullCalendar(); @@ -229,6 +244,12 @@ function action(a) { load(l[0]); } else E.showAlert("Message app not found", "Not found").then(drawWatch); break; + case "[AI:agenda]": + l = require("Storage").list(RegExp("agenda.*app.js")); + if (l.length > 0) { + load(l[0]); + } else E.showAlert("Agenda app not found", "Not found").then(drawWatch); + break; default: l = require("Storage").list(RegExp(a + ".app.js")); if (l.length > 0) { @@ -276,7 +297,6 @@ function input(dir) { drawWatch(); } break; - } } @@ -309,3 +329,10 @@ NRF.on('disconnect', BTevent); dimSeconds = Bangle.isLocked(); drawWatch(); +setWatch(function() { + if (state == "watch") { + Bangle.showLauncher() + } else if (state == "calendar") { + drawWatch(); + } +}, BTN1, {repeat:true, edge:"falling"}); diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 998115827..b84b08575 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.08", + "version": "0.10", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index a406f3cf7..ea613f5c0 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -1,6 +1,6 @@ (function (back) { var FILE = "clockcal.json"; - defaults={ + const defaults={ CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually MODE24: true, //24h mode vs 12h mode @@ -9,19 +9,19 @@ REDSAT: true, // Use red color for saturday? DRAGDOWN: "[AI:messg]", DRAGRIGHT: "[AI:music]", - DRAGLEFT: "[ignore]", + DRAGLEFT: "[AI:agenda]", DRAGUP: "[calend.]" }; - settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); + let settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); - actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"]; + let actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]","[AI:agenda]"]; require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js",""))); function writeSettings() { require('Storage').writeJSON(FILE, settings); } - menu = { + const menu = { "": { "title": "Clock & Calendar" }, "< Back": () => back(), 'Buzz(dis)conn.?': { diff --git a/apps/color_catalog/ChangeLog b/apps/color_catalog/ChangeLog index b79d0c85b..d1d409730 100644 --- a/apps/color_catalog/ChangeLog +++ b/apps/color_catalog/ChangeLog @@ -1 +1,3 @@ 0.01: 1st ver,RGB565 and RGB888 colors in a common UI/UX +0.02: Minor code improvements +0.03: Minor code improvements diff --git a/apps/color_catalog/app.js b/apps/color_catalog/app.js index 58951d1c6..b2f39c7a7 100644 --- a/apps/color_catalog/app.js +++ b/apps/color_catalog/app.js @@ -11,7 +11,7 @@ var v_model=process.env.BOARD; console.log("device="+v_model); var x_max_screen=g.getWidth();//240; - var y_max_screen=g.getHeight(); //240; + //var y_max_screen=g.getHeight(); //240; var y_wg_bottom=g.getHeight()-25; var y_wg_top=25; if (v_model=='BANGLEJS') { @@ -20,7 +20,7 @@ var v_model=process.env.BOARD; var y_btn2=124; //harcoded for bangle.js cuz it is not the half of } else x_max_usable_area=240; - var contador=1; + //var contador=1; var cont_items=0; var cont_row=0; var v_boxes_row=4; @@ -31,26 +31,26 @@ var v_model=process.env.BOARD; var v_font1size=11; var v_fontsize=13; var v_color_b_area='#111111';//black - var v_color_b_area2=0x5AEB;//Dark + //var v_color_b_area2=0x5AEB;//Dark var v_color_text='#FB0E01'; var v_color_statictxt='#e56e06'; //orange RGB format rrggbb //RGB565 requires only 16 (5+6+5) bits/2 bytes - var a_colors_str= Array('White RGB565 0x','Orange','DarkGreen','Yellow', + var a_colors_str= ['White RGB565 0x','Orange','DarkGreen','Yellow', 'Maroon','Blue','green','Purple', 'cyan','olive','DarkCyan','DarkGrey', 'Navy','Red','Magenta','GreenYellow', 'Blush RGB888','pure red','Orange','Grey green', 'D. grey','Almond','Amber','Bone', 'Canary','Aero blue','Camel','Baby pink', - 'Y.Corn','Cultured','Eigengrau','Citrine'); - var a_colors= Array(0xFFFF,0xFD20,0x03E0,0xFFE0, + 'Y.Corn','Cultured','Eigengrau','Citrine']; + var a_colors= [0xFFFF,0xFD20,0x03E0,0xFFE0, 0x7800,0x001F,0x07E0,0x780F, 0x07FF,0x7BE0,0x03EF,0x7BEF, 0x000F,0xF800,0xF81F,0xAFE5, '#DE5D83','#FB0E01','#E56E06','#7E795C', '#404040','#EFDECD','#FFBF00','#E3DAC9', '#FFFF99','#C0E8D5','#C19A6B','#F4C2C2', - '#FBEC5D','#F5F5F5','#16161D','#E4D00A'); + '#FBEC5D','#F5F5F5','#16161D','#E4D00A']; var v_color_lines=0xFFFF; //White hex format diff --git a/apps/color_catalog/metadata.json b/apps/color_catalog/metadata.json index 3146a146f..4d49308ef 100644 --- a/apps/color_catalog/metadata.json +++ b/apps/color_catalog/metadata.json @@ -2,7 +2,7 @@ "id": "color_catalog", "name": "Colors Catalog", "shortName": "Colors Catalog", - "version": "0.01", + "version": "0.03", "description": "Displays RGB565 and RGB888 colors, its name and code in screen.", "icon": "app.png", "tags": "Color,input,buttons,touch,UI", diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog index 54ee389e3..e38a7c5a5 100644 --- a/apps/colorful_clock/ChangeLog +++ b/apps/colorful_clock/ChangeLog @@ -1,3 +1,4 @@ ... 0.03: First update with ChangeLog Added 0.04: Tell clock widgets to hide. +0.05: Minor code improvements diff --git a/apps/colorful_clock/app.js b/apps/colorful_clock/app.js index ba6272e9b..b58892311 100644 --- a/apps/colorful_clock/app.js +++ b/apps/colorful_clock/app.js @@ -120,7 +120,6 @@ let twoPi = 2*Math.PI; let Pi = Math.PI; - let halfPi = Math.PI/2; let sin = Math.sin, cos = Math.cos; diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json index 237acf81c..9e77e12c5 100644 --- a/apps/colorful_clock/metadata.json +++ b/apps/colorful_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "colorful_clock", "name": "Colorful Analog Clock", "shortName":"Colorful Clock", - "version":"0.04", + "version": "0.05", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", diff --git a/apps/configurable_clock/ChangeLog b/apps/configurable_clock/ChangeLog index 9d55c1a91..59708756a 100644 --- a/apps/configurable_clock/ChangeLog +++ b/apps/configurable_clock/ChangeLog @@ -1,3 +1,4 @@ ... 0.02: First update with ChangeLog Added 0.03: Tell clock widgets to hide. +0.04: Minor code improvements diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js index 5c3bf3333..4192954ae 100644 --- a/apps/configurable_clock/app.js +++ b/apps/configurable_clock/app.js @@ -748,7 +748,6 @@ let twoPi = 2*Math.PI, deg2rad = Math.PI/180; let Pi = Math.PI; - let halfPi = Math.PI/2; let sin = Math.sin, cos = Math.cos; diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json index 687a5b212..246a5dc21 100644 --- a/apps/configurable_clock/metadata.json +++ b/apps/configurable_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "configurable_clock", "name": "Configurable Analog Clock", "shortName":"Configurable Clock", - "version":"0.03", + "version": "0.04", "description": "an analog clock with several kinds of faces, hands and colors to choose from", "icon": "app-icon.png", "type": "clock", diff --git a/apps/contacts/ChangeLog b/apps/contacts/ChangeLog index 5560f00bc..a0be5eaf6 100644 --- a/apps/contacts/ChangeLog +++ b/apps/contacts/ChangeLog @@ -1 +1,4 @@ 0.01: New App! +0.02: Minor code improvements +0.03: Minor code improvements +0.04: Allow calling contacts from the app, Refactoring diff --git a/apps/contacts/contacts.app.js b/apps/contacts/contacts.app.js index 85eef625b..34e40927a 100644 --- a/apps/contacts/contacts.app.js +++ b/apps/contacts/contacts.app.js @@ -2,108 +2,102 @@ var Layout = require("Layout"); -const W = g.getWidth(); -const H = g.getHeight(); - var wp = require('Storage').readJSON("contacts.json", true) || []; -// Use this with corrupted contacts -//var wp = []; -var key; /* Shared between functions, typically wp name */ - -function writeContact() { +function writeContacts() { require('Storage').writeJSON("contacts.json", wp); } +function callNumber (number) { + Bluetooth.println(JSON.stringify({ + t:"intent", + target:"activity", + action:"android.intent.action.CALL", + flags:["FLAG_ACTIVITY_NEW_TASK"], + categories:["android.intent.category.DEFAULT"], + data: 'tel:' + number, + })) + +} + function mainMenu() { var menu = { "< Back" : Bangle.load }; - if (Object.keys(wp).length==0) Object.assign(menu, {"NO Contacts":""}); - else for (let id in wp) { - let i = id; - menu[wp[id]["name"]]=()=>{ decode(i); }; + if (!wp.length) { + menu['No Contacts'] = () => {}; + } else { + for (const e of wp) { + const closureE = e; + menu[e.name] = () => showContact(closureE); + } } - menu["Add"]=addCard; - menu["Remove"]=removeCard; + menu["Add"] = addContact; + menu["Remove"] = removeContact; g.clear(); E.showMenu(menu); } -function decode(pin) { - var i = wp[pin]; - var l = i["name"] + "\n" + i["number"]; - var la = new Layout ({ - type:"v", c: [ - {type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: l}, - {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label:"OK", cb:l=>{mainMenu();}} - ], lazy:true}); +function showContact(i) { g.clear(); - la.render(); + (new Layout ({ + type:"v", + c: [ + {type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: i["name"] + "\n" + i["number"]}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "Call", cb: l => callNumber(i['number'])}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "Back to list", cb: mainMenu} + ], + lazy:true + })).render(); } -function showNumpad(text, key_, callback) { - key = key_; - E.showMenu(); - function addDigit(digit) { - key+=digit; - if (1) { - l = text[key.length]; - switch (l) { - case '.': case ' ': case "'": - key+=l; - break; - case 'd': case 'D': default: - break; - } +function showNumpad() { + return new Promise((resolve, reject) => { + let number = '' + E.showMenu(); + function addDigit(digit) { + number += digit; + Bangle.buzz(20); + update(); } - Bangle.buzz(20); - update(); - } - function update() { - g.reset(); - g.clearRect(0,0,g.getWidth(),23); - s = key + text.substr(key.length, 999); - g.setFont("Vector:24").setFontAlign(1,0).drawString(s,g.getWidth(),12); - } - ds="12%"; - var numPad = new Layout ({ - type:"v", c: [{ - type:"v", c: [ - {type:"", height:24}, - {type:"h",filly:1, c: [ - {type:"btn", font:ds, width:58, label:"7", cb:l=>{addDigit("7");}}, - {type:"btn", font:ds, width:58, label:"8", cb:l=>{addDigit("8");}}, - {type:"btn", font:ds, width:58, label:"9", cb:l=>{addDigit("9");}} - ]}, - {type:"h",filly:1, c: [ - {type:"btn", font:ds, width:58, label:"4", cb:l=>{addDigit("4");}}, - {type:"btn", font:ds, width:58, label:"5", cb:l=>{addDigit("5");}}, - {type:"btn", font:ds, width:58, label:"6", cb:l=>{addDigit("6");}} - ]}, - {type:"h",filly:1, c: [ - {type:"btn", font:ds, width:58, label:"1", cb:l=>{addDigit("1");}}, - {type:"btn", font:ds, width:58, label:"2", cb:l=>{addDigit("2");}}, - {type:"btn", font:ds, width:58, label:"3", cb:l=>{addDigit("3");}} - ]}, - {type:"h",filly:1, c: [ - {type:"btn", font:ds, width:58, label:"0", cb:l=>{addDigit("0");}}, - {type:"btn", font:ds, width:58, label:"C", cb:l=>{key=key.slice(0,-1); update();}}, - {type:"btn", font:ds, width:58, id:"OK", label:"OK", cb:callback} + function removeDigit() { + number = number.slice(0, -1); + Bangle.buzz(20); + update(); + } + function update() { + g.reset(); + g.clearRect(0,0,g.getWidth(),23); + g.setFont("Vector:24").setFontAlign(1,0).drawString(number, g.getWidth(),12); + } + const ds="12%"; + const digitBtn = (digit) => ({type:"btn", font:ds, width:58, label:digit, cb:l=>{addDigit(digit);}}); + var numPad = new Layout ({ + type:"v", c: [{ + type:"v", c: [ + {type:"", height:24}, + {type:"h",filly:1, c: [digitBtn("1"), digitBtn("2"), digitBtn("3")]}, + {type:"h",filly:1, c: [digitBtn("4"), digitBtn("5"), digitBtn("6")]}, + {type:"h",filly:1, c: [digitBtn("7"), digitBtn("8"), digitBtn("9")]}, + {type:"h",filly:1, c: [ + {type:"btn", font:ds, width:58, label:"C", cb: removeDigit}, + digitBtn('0'), + {type:"btn", font:ds, width:58, id:"OK", label:"OK", cb: l => resolve(number)} + ]} ]} - ]} - ], lazy:true}); - g.clear(); - numPad.render(); - update(); + ], lazy:true}); + g.clear(); + numPad.render(); + update(); + }); } -function removeCard() { +function removeContact() { var menu = { "" : {title : "Select Contact"}, "< Back" : mainMenu }; - if (Object.keys(wp).length==0) Object.assign(menu, {"No Contacts":""}); + if (wp.length===0) Object.assign(menu, {"No Contacts":""}); else { wp.forEach((val, card) => { const name = wp[card].name; @@ -116,7 +110,7 @@ function removeCard() { {type:"h", c: [ {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{ wp.splice(card, 1); - writeContact(); + writeContacts(); mainMenu(); }}, {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}} @@ -130,55 +124,42 @@ function removeCard() { E.showMenu(menu); } -function askPosition(callback) { - let full = ""; - showNumpad("dddDDDddd", "", function() { - callback(key, ""); - }); -} -function createContact(lat, name) { - let n = {}; - n["name"] = name; - n["number"] = lat; - wp.push(n); - print("add -- contacts", wp); - writeContact(); -} - -function addCardName2(key) { +function addNewContact(name) { g.clear(); - askPosition(function(lat, lon) { - print("position -- ", lat, lon); - createContact(lat, result); + showNumpad().then((number) => { + wp.push({name: name, number: number}); + writeContacts(); mainMenu(); - }); + }) + + + } -function addCardName(key) { - result = key; - if (wp[result]!=undefined) { - E.showMenu(); - var alreadyExists = new Layout ( - {type:"v", c: [ - {type:"txt", font:Math.min(15,100/result.length)+"%", pad:1, fillx:1, filly:1, label:result}, - {type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."}, - {type:"h", c: [ - {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addCardName2(key); }}, - {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}} - ]} - ], lazy:true}); - g.clear(); - alreadyExists.render(); +function tryAddContact(name) { + if (wp.filter((e) => e.name === name).length) { + E.showMenu(); + var alreadyExists = new Layout ( + {type:"v", c: [ + {type:"txt", font:Math.min(15,100/name.length)+"%", pad:1, fillx:1, filly:1, label:name}, + {type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."}, + {type:"h", c: [ + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addNewContact(name); }}, + {type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + alreadyExists.render(); return; } - addCardName2(key); + addNewContact(name); } -function addCard() { - require("textinput").input({text:""}).then(result => { - if (result != "") { - addCardName(result); +function addContact() { + require("textinput").input({text:""}).then(name => { + if (name !== "") { + tryAddContact(name); } else mainMenu(); }); diff --git a/apps/contacts/metadata.json b/apps/contacts/metadata.json index 1228fd0bf..50354f74f 100644 --- a/apps/contacts/metadata.json +++ b/apps/contacts/metadata.json @@ -1,6 +1,6 @@ { "id": "contacts", "name": "Contacts", - "version":"0.01", + "version": "0.04", "description": "Provides means of storing user contacts, viewing/editing them on device and from the App loader", "icon": "app.png", "tags": "tool", diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog index 2fa26b89a..6078b4ff3 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -11,3 +11,4 @@ 0.29: Fixed a bug that would leave old font files in storage. 0.30: Added options to show widgets and date on twist and tap. New fonts. 0.31: Bugfix, no more freeze. +0.32: Minor code improvements diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index 1a510f014..55d29a3dd 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -2,8 +2,8 @@ let drawTimeout; let extrasTimeout; let onLock; - let onTap; - let onTwist; + //let onTap; + //let onTwist; let settings = require('Storage').readJSON("contourclock.json", true) || {}; if (settings.fontIndex == undefined) { settings.fontIndex = 0; @@ -60,7 +60,6 @@ drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); - let date = new Date(); g.reset(); if (extrasShown) drawExtras(); else hideExtras(); diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index 5c97ce933..61c0f5643 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,7 +1,7 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.31", + "version": "0.32", "icon": "app.png", "readme": "README.md", "description": "A Minimalist clockface with large Digits.", diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index 7386bbc35..30c775a49 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -2,3 +2,4 @@ 0.02: Cleanup interface and add settings, widget, add skin temp reporting. 0.03: Move code for recording to this app 0.04: Use default Bangle formatter for booleans +0.05: Minor code improvements diff --git a/apps/coretemp/coretemp.js b/apps/coretemp/coretemp.js index 7cbbe3577..0337891e1 100644 --- a/apps/coretemp/coretemp.js +++ b/apps/coretemp/coretemp.js @@ -1,6 +1,6 @@ // Simply listen for core events and show data -var btm = g.getHeight() - 1; +//var btm = g.getHeight() - 1; var px = g.getWidth() / 2; // Dark or light logo diff --git a/apps/coretemp/metadata.json b/apps/coretemp/metadata.json index 87cb42722..2b7de0bf0 100644 --- a/apps/coretemp/metadata.json +++ b/apps/coretemp/metadata.json @@ -1,7 +1,7 @@ { "id": "coretemp", "name": "CoreTemp", - "version": "0.04", + "version": "0.05", "description": "Display CoreTemp device sensor data", "icon": "coretemp.png", "type": "app", 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/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 5264e8d42..30bfdd560 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -7,3 +7,8 @@ Improve connection code 0.07: Make Bangle.js 2 compatible 0.08: Convert Yes/No On/Off in settings to checkboxes +0.09: Automatically reconnect on error +0.10: Fix cscsensor when using coospoo sensor that supports crank *and* wheel +0.11: Update to use blecsc library +0.12: Fix regression reporting cadence (reported per second when should be per minute) (fix #3434) +0.13: Fix total distance calculation \ No newline at end of file diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js index 4ebe7d57e..6a8ca8b0f 100644 --- a/apps/cscsensor/cscsensor.app.js +++ b/apps/cscsensor/cscsensor.app.js @@ -1,8 +1,3 @@ -var device; -var gatt; -var service; -var characteristic; - const SETTINGS_FILE = 'cscsensor.json'; const storage = require('Storage'); const W = g.getWidth(); @@ -17,12 +12,10 @@ class CSCSensor { constructor() { this.movingTime = 0; this.lastTime = 0; - this.lastBangleTime = Date.now(); this.lastRevs = -1; this.settings = storage.readJSON(SETTINGS_FILE, 1) || {}; this.settings.totaldist = this.settings.totaldist || 0; this.totaldist = this.settings.totaldist; - this.wheelCirc = (this.settings.wheelcirc || 2230)/25.4; this.speedFailed = 0; this.speed = 0; this.maxSpeed = 0; @@ -34,8 +27,6 @@ class CSCSensor { this.distFactor = this.qMetric ? 1.609344 : 1; this.screenInit = true; this.batteryLevel = -1; - this.lastCrankTime = 0; - this.lastCrankRevs = 0; this.showCadence = false; this.cadence = 0; } @@ -63,10 +54,6 @@ class CSCSensor { } } - updateBatteryLevel(event) { - if (event.target.uuid == "0x2a19") this.setBatteryLevel(event.target.value.getUint8(0)); - } - drawBatteryIcon() { g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H) .fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H) @@ -81,7 +68,7 @@ class CSCSensor { } updateScreenRevs() { - var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; + var dist = this.distFactor*(this.lastRevs-this.lastRevsStart) * csc.settings.circum/*mm*/ / 1000000; var ddist = Math.round(100*dist)/100; var tdist = Math.round(this.distFactor*this.totaldist*10)/10; var dspeed = Math.round(10*this.distFactor*this.speed)/10; @@ -157,113 +144,38 @@ class CSCSensor { } } - updateSensor(event) { - var qChanged = false; - if (event.target.uuid == "0x2a5b") { - if (event.target.value.getUint8(0, true) & 0x2) { - // crank revolution - if enabled - const crankRevs = event.target.value.getUint16(1, true); - const crankTime = event.target.value.getUint16(3, true); - if (crankTime > this.lastCrankTime) { - this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024); - qChanged = true; - } - this.lastCrankRevs = crankRevs; - this.lastCrankTime = crankTime; - } else { - // wheel revolution - var wheelRevs = event.target.value.getUint32(1, true); - var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); - if (dRevs>0) { - qChanged = true; - this.totaldist += dRevs*this.wheelCirc/63360.0; - if ((this.totaldist-this.settings.totaldist)>0.1) { - this.settings.totaldist = this.totaldist; - storage.writeJSON(SETTINGS_FILE, this.settings); - } - } - this.lastRevs = wheelRevs; - if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; - var wheelTime = event.target.value.getUint16(5, true); - var dT = (wheelTime-this.lastTime)/1024; - var dBT = (Date.now()-this.lastBangleTime)/1000; - this.lastBangleTime = Date.now(); - if (dT<0) dT+=64; - if (Math.abs(dT-dBT)>3) dT = dBT; - this.lastTime = wheelTime; - this.speed = this.lastSpeed; - if (dRevs>0 && dT>0) { - this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; - this.speedFailed = 0; - this.movingTime += dT; - } else if (!this.showCadence) { - this.speedFailed++; - qChanged = false; - if (this.speedFailed>3) { - this.speed = 0; - qChanged = (this.lastSpeed>0); - } - } - this.lastSpeed = this.speed; - if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; - } - } - if (qChanged) this.updateScreen(); - } } var mySensor = new CSCSensor(); -function getSensorBatteryLevel(gatt) { - gatt.getPrimaryService("180f").then(function(s) { - return s.getCharacteristic("2a19"); - }).then(function(c) { - c.on('characteristicvaluechanged', (event)=>mySensor.updateBatteryLevel(event)); - return c.startNotifications(); - }); -} +var csc = require("blecsc").getInstance(); +csc.on("data", e => { + mySensor.totaldist += e.wr * csc.settings.circum/*mm*/ / 1000000; // finally in km + mySensor.lastRevs = e.cwr; + if (mySensor.lastRevsStart<0) mySensor.lastRevsStart = e.cwr; + mySensor.speed = e.kph; + mySensor.movingTime += e.wdt; + if (mySensor.speed>mySensor.maxSpeed && (mySensor.movingTime>3 || mySensor.speed<20) && mySensor.speed<50) + mySensor.maxSpeed = mySensor.speed; + mySensor.cadence = e.crps*60; + mySensor.updateScreen(); + mySensor.updateScreen(); +}); -function connection_setup() { - mySensor.screenInit = true; - E.showMessage("Scanning for CSC sensor..."); - NRF.requestDevice({ filters: [{services:["1816"]}]}).then(function(d) { - device = d; - E.showMessage("Found device"); - return device.gatt.connect(); - }).then(function(ga) { - gatt = ga; - E.showMessage("Connected"); - return gatt.getPrimaryService("1816"); - }).then(function(s) { - service = s; - return service.getCharacteristic("2a5b"); - }).then(function(c) { - characteristic = c; - characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); - return characteristic.startNotifications(); - }).then(function() { - console.log("Done!"); - g.reset().clearRect(Bangle.appRect).flip(); - getSensorBatteryLevel(gatt); - mySensor.updateScreen(); - }).catch(function(e) { - E.showMessage(e.toString(), "ERROR"); - console.log(e); - }); -} - -connection_setup(); +csc.on("status", txt => { + //print("->", txt); + E.showMessage(txt); +}); E.on('kill',()=>{ - if (gatt!=undefined) gatt.disconnect(); + csc.stop(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); -NRF.on('disconnect', connection_setup); // restart if disconnected Bangle.setUI("updown", d=>{ if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } - else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } - else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } + else if (!d) { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } }); Bangle.loadWidgets(); Bangle.drawWidgets(); +csc.start(); // start a connection \ No newline at end of file diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index d7c3add53..0029c4b82 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,10 +2,11 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.08", + "version": "0.13", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth,bike,cycle,bicycle", + "dependencies" : { "blecsc":"module" }, "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ 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/cycling/ChangeLog b/apps/cycling/ChangeLog index ec66c5568..9fec754fc 100644 --- a/apps/cycling/ChangeLog +++ b/apps/cycling/ChangeLog @@ -1 +1,3 @@ 0.01: Initial version +0.02: Minor code improvements +0.03: Move blecsc library into its own app so it can be shared (and fix some issues) \ No newline at end of file diff --git a/apps/cycling/README.md b/apps/cycling/README.md index 7ba8ee224..485537293 100644 --- a/apps/cycling/README.md +++ b/apps/cycling/README.md @@ -1,4 +1,5 @@ # Cycling + > Displays data from a BLE Cycling Speed and Cadence sensor. *This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.* @@ -27,8 +28,5 @@ Inside the Cycling app, use button / tap screen to: ## TODO * Sensor battery status * Implement crank events / show cadence -* Bangle.js 1 compatibility * Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike) -## Development -There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage. diff --git a/apps/cycling/blecsc-emu.js b/apps/cycling/blecsc-emu.js deleted file mode 100644 index ca5058545..000000000 --- a/apps/cycling/blecsc-emu.js +++ /dev/null @@ -1,111 +0,0 @@ -// UUID of the Bluetooth CSC Service -const SERVICE_UUID = "1816"; -// UUID of the CSC measurement characteristic -const MEASUREMENT_UUID = "2a5b"; - -// Wheel revolution present bit mask -const FLAGS_WREV_BM = 0x01; -// Crank revolution present bit mask -const FLAGS_CREV_BM = 0x02; - -/** - * Fake BLECSC implementation for the emulator, where it's hard to test - * with actual hardware. Generates "random" wheel events (no crank). - * - * To upload as a module, paste the entire file in the console using this - * command: require("Storage").write("blecsc-emu",``); - */ -class BLECSCEmulator { - constructor() { - this.timeout = undefined; - this.interval = 500; - this.ccr = 0; - this.lwt = 0; - this.handlers = { - // value - // disconnect - // wheelEvent - // crankEvent - }; - } - - getDeviceAddress() { - return 'fa:ke:00:de:vi:ce'; - } - - /** - * Callback for the GATT characteristicvaluechanged event. - * Consumers must not call this method! - */ - onValue(event) { - // Not interested in non-CSC characteristics - if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; - - // Notify the generic 'value' handler - if (this.handlers.value) this.handlers.value(event); - - const flags = event.target.value.getUint8(0, true); - // Notify the 'wheelEvent' handler - if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ - cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions - lwet: event.target.value.getUint16(5, true), // last wheel event time - }); - - // Notify the 'crankEvent' handler - if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ - ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions - lcet: event.target.value.getUint16(9, true), // last crank event time - }); - } - - /** - * Register an event handler. - * - * @param {string} event value|disconnect - * @param {function} handler handler function that receives the event as its first argument - */ - on(event, handler) { - this.handlers[event] = handler; - } - - fakeEvent() { - this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20)); - this.lwt = (this.lwt + this.interval) % 0x10000; - this.ccr++; - - var buffer = new ArrayBuffer(8); - var view = new DataView(buffer); - view.setUint8(0, 0x01); // Wheel revolution data present bit - view.setUint32(1, this.ccr, true); // Cumulative crank revolutions - view.setUint16(5, this.lwt, true); // Last wheel event time - - this.onValue({ - target: { - uuid: "0x2a5b", - value: view, - }, - }); - - this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); - } - - /** - * Find and connect to a device which exposes the CSC service. - * - * @return {Promise} - */ - connect() { - this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); - return Promise.resolve(true); - } - - /** - * Disconnect the device. - */ - disconnect() { - if (!this.timeout) return; - clearTimeout(this.timeout); - } -} - -exports = BLECSCEmulator; diff --git a/apps/cycling/blecsc.js b/apps/cycling/blecsc.js deleted file mode 100644 index 7a47108e5..000000000 --- a/apps/cycling/blecsc.js +++ /dev/null @@ -1,150 +0,0 @@ -const SERVICE_UUID = "1816"; -// UUID of the CSC measurement characteristic -const MEASUREMENT_UUID = "2a5b"; - -// Wheel revolution present bit mask -const FLAGS_WREV_BM = 0x01; -// Crank revolution present bit mask -const FLAGS_CREV_BM = 0x02; - -/** - * This class 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 - * - \`wheelEvent\` - the peripharial sends a notification containing wheel event data - * - \`crankEvent\` - the peripharial sends a notification containing crank event data - * - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event) - * - \`disconnect\` - the peripherial ends the connection or the connection is lost - * - * Each event can only have one handler. Any call to \`on()\` will - * replace a previously registered handler for the same event. - */ -class BLECSC { - constructor() { - this.device = undefined; - this.ccInterval = undefined; - this.gatt = undefined; - this.handlers = { - // wheelEvent - // crankEvent - // value - // disconnect - }; - } - - getDeviceAddress() { - if (!this.device || !this.device.id) - return '00:00:00:00:00:00'; - return this.device.id.split(" ")[0]; - } - - checkConnection() { - if (!this.device) - console.log("no device"); - // else - // console.log("rssi: " + this.device.rssi); - } - - /** - * Callback for the GATT characteristicvaluechanged event. - * Consumers must not call this method! - */ - onValue(event) { - // Not interested in non-CSC characteristics - if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; - - // Notify the generic 'value' handler - if (this.handlers.value) this.handlers.value(event); - - const flags = event.target.value.getUint8(0, true); - // Notify the 'wheelEvent' handler - if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ - cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions - lwet: event.target.value.getUint16(5, true), // last wheel event time - }); - - // Notify the 'crankEvent' handler - if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ - ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions - lcet: event.target.value.getUint16(9, true), // last crank event time - }); - } - - /** - * Callback for the NRF disconnect event. - * Consumers must not call this method! - */ - onDisconnect(event) { - console.log("disconnected"); - if (this.ccInterval) - clearInterval(this.ccInterval); - - if (!this.handlers.disconnect) return; - this.handlers.disconnect(event); - } - - /** - * Register an event handler. - * - * @param {string} event wheelEvent|crankEvent|value|disconnect - * @param {function} handler function that will receive the event as its first argument - */ - on(event, handler) { - this.handlers[event] = handler; - } - - /** - * Find and connect to a device which exposes the CSC service. - * - * @return {Promise} - */ - connect() { - // Register handler for the disconnect event to be passed throug - NRF.on('disconnect', this.onDisconnect.bind(this)); - - // Find a device, then get the CSC Service and subscribe to - // notifications on the CSC Measurement characteristic. - // NRF.setLowPowerConnection(true); - return NRF.requestDevice({ - timeout: 5000, - filters: [{ services: [SERVICE_UUID] }], - }).then(device => { - this.device = device; - this.device.on('gattserverdisconnected', this.onDisconnect.bind(this)); - this.ccInterval = setInterval(this.checkConnection.bind(this), 2000); - return device.gatt.connect(); - }).then(gatt => { - this.gatt = gatt; - return gatt.getPrimaryService(SERVICE_UUID); - }).then(service => { - return service.getCharacteristic(MEASUREMENT_UUID); - }).then(characteristic => { - characteristic.on('characteristicvaluechanged', this.onValue.bind(this)); - return characteristic.startNotifications(); - }); - } - - /** - * Disconnect the device. - */ - disconnect() { - if (this.ccInterval) - clearInterval(this.ccInterval); - - if (!this.gatt) return; - try { - this.gatt.disconnect(); - } catch { - // - } - } -} - -exports = BLECSC; diff --git a/apps/cycling/cycling.app.js b/apps/cycling/cycling.app.js index 268284a29..7261d3519 100644 --- a/apps/cycling/cycling.app.js +++ b/apps/cycling/cycling.app.js @@ -23,7 +23,6 @@ class CSCSensor { // CSC runtime variables this.movingTime = 0; // unit: s this.lastBangleTime = Date.now(); // unit: ms - this.lwet = 0; // last wheel event time (unit: s/1024) this.cwr = -1; // cumulative wheel revolutions this.cwrTrip = 0; // wheel revolutions since trip start this.speed = 0; // unit: m/s @@ -84,7 +83,7 @@ class CSCSensor { console.log("Trying to connect to BLE CSC"); // Hook up events - this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this)); + this.blecsc.on('data', this.onWheelEvent.bind(this)); this.blecsc.on('disconnect', this.onDisconnect.bind(this)); // Scan for BLE device and connect @@ -171,20 +170,11 @@ class CSCSensor { // Increment the trip revolutions counter this.cwrTrip += dRevs; - // Calculate time delta since last wheel event - var dT = (event.lwet - this.lwet)/1024; - var now = Date.now(); - var dBT = (now-this.lastBangleTime)/1000; - this.lastBangleTime = now; - if (dT<0) dT+=64; // wheel event time wraps every 64s - if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this - this.lwet = event.lwet; - // Recalculate current speed - if (dRevs>0 && dT>0) { - this.speed = dRevs * this.wheelCirc / dT; + if (dRevs>0 ) { + this.speed = event.wrps * this.wheelCirc; this.speedFailed = 0; - this.movingTime += dT; + this.movingTime += event.wdt; } else { this.speedFailed++; if (this.speedFailed>3) { @@ -429,15 +419,7 @@ class CSCDisplay { } } -var BLECSC; -if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") { - // Emulator - BLECSC = require("blecsc-emu"); -} else { - // Actual hardware - BLECSC = require("blecsc"); -} -var blecsc = new BLECSC(); +var blecsc = require("blecsc").getInstance(); var display = new CSCDisplay(); var sensor = new CSCSensor(blecsc, display); diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json index caf93eda3..51e51b409 100644 --- a/apps/cycling/metadata.json +++ b/apps/cycling/metadata.json @@ -2,16 +2,16 @@ "id": "cycling", "name": "Bangle Cycling", "shortName": "Cycling", - "version": "0.01", + "version": "0.03", "description": "Display live values from a BLE CSC sensor", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", + "dependencies" : { "blecsc":"module" }, "supports": ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"cycling.app.js","url":"cycling.app.js"}, {"name":"cycling.settings.js","url":"settings.js"}, - {"name":"blecsc","url":"blecsc.js"}, {"name":"cycling.img","url":"cycling.icon.js","evaluate": true} ], "data": [ diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 751164c07..3d8ef0472 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -8,3 +8,7 @@ 0.08: fix idle timer always getting set to true 0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.10: Use widget_utils. +0.11: Minor code improvements +0.12: Added setting to change Battery estimate to hours +0.13: Fixed Battery estimate Default to percentage and improved setting string +0.14: Use `power_usage` module diff --git a/apps/daisy/README.md b/apps/daisy/README.md index 491ed697f..5599d313c 100644 --- a/apps/daisy/README.md +++ b/apps/daisy/README.md @@ -10,7 +10,7 @@ Forum](http://forum.espruino.com/microcosms/1424/) * Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) * Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer -* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate) +* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate) * The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle * The heart value is displayed in RED if the confidence value is less than 50% * NOTE: The heart rate monitor of Bangle JS 2 is not very accurate when moving about. @@ -20,6 +20,7 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248) [MyLocation](https://banglejs.com/apps/?id=mylocation) * The screen is updated every minute to save battery power * Uses the [BloggerSansLight](https://www.1001fonts.com/rounded-fonts.html?page=3) font, which if free for commercial use +* You need to run >2V22 to show the battery estimate in hours ## Future Development * Use mini icons in the information line rather that text diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 3b3975105..4aa7eb132 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,6 +1,4 @@ var SunCalc = require("suncalc"); // from modules folder -const storage = require('Storage'); -const locale = require("locale"); const widget_utils = require('widget_utils'); const SETTINGS_FILE = "daisy.json"; const LOCATION_FILE = "mylocation.json"; @@ -85,6 +83,7 @@ function loadSettings() { settings.gy = settings.gy||'#020'; settings.fg = settings.fg||'#0f0'; settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); + settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours); assignPalettes(); } @@ -114,13 +113,39 @@ function updateSunRiseSunSet(now, lat, lon, line){ sunSet = extractTime(times.sunset); } +function batteryString(){ + let stringToInsert; + if (settings.batt_hours) { + var batt_usage = require("power_usage").get().hrsLeft; + let rounded; + if (batt_usage > 24) { + var days = Math.floor(batt_usage/24); + var hours = Math.round((batt_usage/24 - days) * 24); + stringToInsert = '\n' + days + ((days < 2) ? 'd' : 'ds') + ' ' + hours + ((hours < 2) ? 'h' : 'hs'); + } + else if (batt_usage > 9) { + rounded = Math.round(200000/E.getPowerUsage().total * 10) / 10; + } + else { + rounded = Math.round(200000/E.getPowerUsage().total * 100) / 100; + } + if (batt_usage < 24) { + stringToInsert = '\n' + rounded + ' ' + ((batt_usage < 2) ? 'h' : 'hs'); + } + } + else{ + stringToInsert = ' ' + E.getBattery() + '%'; + } + return 'BATTERY' + stringToInsert; +} + const infoData = { ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, ID_SR: { calc: () => 'SUNRISE ' + sunRise }, ID_SS: { calc: () => 'SUNSET ' + sunSet }, ID_STEP: { calc: () => 'STEPS ' + getSteps() }, - ID_BATT: { calc: () => 'BATTERY ' + E.getBattery() + '%' }, + ID_BATT: { calc: batteryString}, ID_HRM: { calc: () => hrmCurrent } }; @@ -196,9 +221,9 @@ function draw() { function drawClock() { var date = new Date(); - var timeStr = require("locale").time(date,1); + //var timeStr = require("locale").time(date,1); var da = date.toString().split(" "); - var time = da[4].substr(0,5); + //var time = da[4].substr(0,5); var hh = da[4].substr(0,2); var mm = da[4].substr(3,2); var steps = getSteps(); diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 471f8e56f..178cd8b20 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.10", + "version": "0.14", "dependencies": {"mylocation":"app"}, "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", diff --git a/apps/daisy/settings.js b/apps/daisy/settings.js index 6397a81f4..c0a2ffeea 100644 --- a/apps/daisy/settings.js +++ b/apps/daisy/settings.js @@ -5,7 +5,8 @@ let s = {'gy' : '#020', 'fg' : '#0f0', 'color': 'Green', - 'check_idle' : true}; + 'check_idle' : true, + 'batt_hours' : false}; // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -45,6 +46,14 @@ s.idle_check = v; save(); }, + }, + 'Expected Battery Life In Days Not Percentage': { + value: !!s.batt_hours, + onchange: v => { + s.batt_hours = v; + save(); + }, } }); }) + diff --git a/apps/dane_tcr/ChangeLog b/apps/dane_tcr/ChangeLog index 69424b1f4..05ef79052 100644 --- a/apps/dane_tcr/ChangeLog +++ b/apps/dane_tcr/ChangeLog @@ -6,3 +6,4 @@ 0.06: remove app image as it is unused 0.07: Bump version number for change to apps.json causing 404 on upload 0.08: Use default Bangle formatter for booleans +0.09: Minor code improvements diff --git a/apps/dane_tcr/app.js b/apps/dane_tcr/app.js index ce75c55cb..ce8c98025 100644 --- a/apps/dane_tcr/app.js +++ b/apps/dane_tcr/app.js @@ -1,11 +1,6 @@ var d = require("dane_arwes"); var Arwes = d.default(); -const yOffset = 23; -const width = g.getWidth(); -const height = g.getHeight(); -const xyCenter = width / 2 + 4; - const Storage = require("Storage"); const filename = 'dane_tcr.json'; let settings = Storage.readJSON(filename,1) || { diff --git a/apps/dane_tcr/metadata.json b/apps/dane_tcr/metadata.json index 5527c846d..c6a649f0e 100644 --- a/apps/dane_tcr/metadata.json +++ b/apps/dane_tcr/metadata.json @@ -2,7 +2,7 @@ "id": "dane_tcr", "name": "DANE Touch Launcher", "shortName": "DANE Toucher", - "version": "0.08", + "version": "0.09", "description": "Touch enable left to right launcher in the style of the DANE Watchface", "icon": "app.png", "type": "launch", 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/delaylock/ChangeLog b/apps/delaylock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/delaylock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/delaylock/README.md b/apps/delaylock/README.md new file mode 100644 index 000000000..da2ef3cda --- /dev/null +++ b/apps/delaylock/README.md @@ -0,0 +1,23 @@ +# Delayed Locking + +Delay the locking of the touchscreen to 5 seconds after the backlight turns off. Giving you the chance to interact with the watch without having to press the hardware button again. + +## Usage + +Just install and the behavior is tweaked at boot time. + +## Features + +- respects the LCD Timeout and Brightness as configured in the settings app. + +## Requests + +Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions. + +## Creator + +thyttan + +## Acknowledgements + +Inspired by the conversation between Gordon Williams and user156427 linked here: https://forum.espruino.com/conversations/392219/ diff --git a/apps/delaylock/app.png b/apps/delaylock/app.png new file mode 100644 index 000000000..7bdce945d Binary files /dev/null and b/apps/delaylock/app.png differ diff --git a/apps/delaylock/boot.js b/apps/delaylock/boot.js new file mode 100644 index 000000000..87dcbf186 --- /dev/null +++ b/apps/delaylock/boot.js @@ -0,0 +1,21 @@ +{ + let backlightTimeout = Bangle.getOptions().backlightTimeout; + let brightness = require("Storage").readJSON("setting.json", true); + brightness = brightness?brightness.brightness:1; + + Bangle.setOptions({ + backlightTimeout: backlightTimeout, + lockTimeout: backlightTimeout+5000 + }); + + let turnLightsOn = (_,numOrObj)=>{ + if (!Bangle.isBacklightOn()) { + Bangle.setLCDPower(brightness); + if (typeof numOrObj !== "number") E.stopEventPropagation(); // Touches will not be passed on to other listeners, but swipes will. + } + }; + + setWatch(turnLightsOn, BTN1, { repeat: true, edge: 'rising' }); + Bangle.prependListener("swipe", turnLightsOn); + Bangle.prependListener("touch", turnLightsOn); +} diff --git a/apps/delaylock/metadata.json b/apps/delaylock/metadata.json new file mode 100644 index 000000000..dff4d9219 --- /dev/null +++ b/apps/delaylock/metadata.json @@ -0,0 +1,13 @@ +{ "id": "delaylock", + "name": "Delayed Locking", + "version":"0.01", + "description": "Delay the locking of the screen to 5 seconds after the backlight turns off.", + "icon": "app.png", + "tags": "settings, configuration, backlight, touchscreen, screen", + "type": "bootloader", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"delaylock.boot.js","url":"boot.js"} + ] +} diff --git a/apps/devstopwatch/ChangeLog b/apps/devstopwatch/ChangeLog index 7e90e061e..11567d141 100644 --- a/apps/devstopwatch/ChangeLog +++ b/apps/devstopwatch/ChangeLog @@ -5,4 +5,6 @@ realigned quick n dirty screen positions help adjusted to fit bangle1 & bangle2 screen-size with widgets fixed bangle2 colors for chrono and last lap highlight - added screen for bangle2 and a small README \ No newline at end of file + added screen for bangle2 and a small README +0.05: Minor code improvements +0.06: Minor code improvements diff --git a/apps/devstopwatch/app.js b/apps/devstopwatch/app.js index d2a4b1117..747573c0c 100644 --- a/apps/devstopwatch/app.js +++ b/apps/devstopwatch/app.js @@ -12,7 +12,7 @@ const FONT = '6x8'; const CHRONO = '/* C H R O N O */'; -var reset = false; +//var reset = false; var currentLap = ''; var chronoInterval; @@ -43,7 +43,7 @@ Bangle.setUI("clockupdown", btn=>{ function resetChrono() { state.laps = [EMPTY_H, EMPTY_H, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP]; state.started = false; - reset = true; + //reset = true; state.currentLapIndex = 1; currentLap = ''; @@ -61,11 +61,11 @@ function chronometer() { state.whenStarted = rightNow; state.whenStartedTotal = rightNow; state.started = true; - reset = false; + //reset = false; } currentLap = calculateLap(state.whenStarted); - total = calculateLap(state.whenStartedTotal); + const total = calculateLap(state.whenStartedTotal); state.laps[0] = total; state.laps[1] = currentLap; @@ -123,7 +123,7 @@ function printChrono() { g.setColor(g.theme.fg); let suffix = ' '; if (state.currentLapIndex === i) { - let suffix = '*'; + let suffix = '*'; //TODO: Should `let` be removed here? if (process.env.HWVERSION==2) g.setColor("#0ee"); else g.setColor("#f70"); } diff --git a/apps/devstopwatch/metadata.json b/apps/devstopwatch/metadata.json index c4b6c7a67..f8e3fe106 100644 --- a/apps/devstopwatch/metadata.json +++ b/apps/devstopwatch/metadata.json @@ -2,7 +2,7 @@ "id": "devstopwatch", "name": "Dev Stopwatch", "shortName": "Dev Stopwatch", - "version": "0.04", + "version": "0.06", "description": "Stopwatch with 5 laps supported (cyclically replaced)", "icon": "app.png", "tags": "stopwatch,chrono,timer,chronometer", diff --git a/apps/diceroll/ChangeLog b/apps/diceroll/ChangeLog index 89dff4011..284e78368 100644 --- a/apps/diceroll/ChangeLog +++ b/apps/diceroll/ChangeLog @@ -1 +1,2 @@ -0.01: App created \ No newline at end of file +0.01: App created +0.02: Minor code improvements diff --git a/apps/diceroll/app.js b/apps/diceroll/app.js index d514ce92f..61a3d9917 100644 --- a/apps/diceroll/app.js +++ b/apps/diceroll/app.js @@ -105,4 +105,4 @@ function main() { Bangle.setLCDPower(1); } -var interval = setInterval(main, 300); \ No newline at end of file +setInterval(main, 300); \ No newline at end of file diff --git a/apps/diceroll/metadata.json b/apps/diceroll/metadata.json index 81a2f8bfd..256ad8a80 100644 --- a/apps/diceroll/metadata.json +++ b/apps/diceroll/metadata.json @@ -2,7 +2,7 @@ "name": "Dice-n-Roll", "shortName":"Dice-n-Roll", "icon": "app.png", - "version":"0.01", + "version": "0.02", "description": "A dice app with a few different dice.", "screenshots": [{"url":"diceroll_screenshot.png"}], "tags": "game", diff --git a/apps/diract/ChangeLog b/apps/diract/ChangeLog index 34fc73a76..272d01ab8 100644 --- a/apps/diract/ChangeLog +++ b/apps/diract/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Tweaked proximity identification settings +0.03: Minor code improvements diff --git a/apps/diract/diract.js b/apps/diract/diract.js index 69f0a88e4..d4effca89 100644 --- a/apps/diract/diract.js +++ b/apps/diract/diract.js @@ -74,6 +74,7 @@ let digestTime = new Uint8Array([ 0, 0, 0 ]); let numberOfDigestPages = 0; let sensorData = [ 0x82, 0x08, 0x3f ]; let cyclicCount = 0; +let encodedBattery = 0; let lastDigestTime = Math.round(getTime()); let lastResetTime = Math.round(getTime()); let isExciterPresent = false; @@ -517,7 +518,7 @@ function updateSensorData() { encodedBattery = encodeBatteryPercentage(); } - encodedAcceleration = encodeAcceleration(); + let encodedAcceleration = encodeAcceleration(); sensorData[0] = ((encodedAcceleration.x << 2) & 0xfc) | ((encodedAcceleration.y >> 4) & 0x3f); diff --git a/apps/diract/metadata.json b/apps/diract/metadata.json index af9406e91..2b6cd810e 100644 --- a/apps/diract/metadata.json +++ b/apps/diract/metadata.json @@ -2,7 +2,7 @@ "id": "diract", "name": "DirAct", "shortName": "DirAct", - "version": "0.02", + "version": "0.03", "description": "Proximity interaction detection.", "icon": "diract.png", "type": "app", diff --git a/apps/distortclk/ChangeLog b/apps/distortclk/ChangeLog index 4c7291526..11be002af 100644 --- a/apps/distortclk/ChangeLog +++ b/apps/distortclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New face! 0.02: Improved clock +0.03: Minor code improvements diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js index a9fdd1ef2..715899fbb 100644 --- a/apps/distortclk/app.js +++ b/apps/distortclk/app.js @@ -26,7 +26,7 @@ function time() { var d = new Date(); var day = d.getDate(); var time = require("locale").time(d,1); - var date = require("locale").date(d); + //var date = require("locale").date(d); var mo = require("date_utils").month(d.getMonth()+1,0); g.setFontAlign(0,0); diff --git a/apps/distortclk/metadata.json b/apps/distortclk/metadata.json index 125dac590..cd1bf9d4d 100644 --- a/apps/distortclk/metadata.json +++ b/apps/distortclk/metadata.json @@ -2,7 +2,7 @@ "id": "distortclk", "name": "Distort Clock", "shortName":"Distort Clock", - "version": "0.02", + "version": "0.03", "description": "A clockface", "icon": "app.png", "type": "clock", diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog index 563db87e7..cb2e8bd49 100644 --- a/apps/dotclock/ChangeLog +++ b/apps/dotclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Based on the Analog Clock app, minimal dot 0.02: Remove hardcoded hour buzz (you can install widchime if you miss it) 0.03: Use setUI, adjust for themes and different size screens +0.04: Minor code improvements diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js index 66255d1b4..0127cd488 100644 --- a/apps/dotclock/clock-dot.js +++ b/apps/dotclock/clock-dot.js @@ -1,6 +1,5 @@ const big = g.getWidth()>200; const locale = require('locale'); -const p = Math.PI / 2; const pRad = Math.PI / 180; let timer = null; let currentDate = new Date(); diff --git a/apps/dotclock/metadata.json b/apps/dotclock/metadata.json index 396e63917..e8d7415fd 100644 --- a/apps/dotclock/metadata.json +++ b/apps/dotclock/metadata.json @@ -1,7 +1,7 @@ { "id": "dotclock", "name": "Dot Clock", - "version": "0.03", + "version": "0.04", "description": "A Minimal Dot Analog Clock", "icon": "clock-dot.png", "type": "clock", diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog index 77d82eff9..dc73a3c23 100644 --- a/apps/doztime/ChangeLog +++ b/apps/doztime/ChangeLog @@ -5,3 +5,5 @@ 0.05: extraneous comments and code removed display improved now supports Adjust Clock widget, if installed +0.06: Minor code improvements +0.07: Bangle2: Shift the position of one line on the screen diff --git a/apps/doztime/app-bangle1.js b/apps/doztime/app-bangle1.js index 38c5acbac..a176ef270 100644 --- a/apps/doztime/app-bangle1.js +++ b/apps/doztime/app-bangle1.js @@ -164,7 +164,7 @@ function drawTime() x = 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); let msg = "00000"+Math.floor(x).toString(12); - let time = msg.substr(-5,3)+"."+msg.substr(-2); + let time = msg.substr(-5,3)+"."+msg.substr(-2); //TODO: should `time` and `wait` have been defined outside the if block? let wait = 347*(1-(x%1)); timeDef = time6; } else { @@ -210,8 +210,8 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); // Functions for weather mode - TODO -function drawWeather() {} -function modeWeather() {} +//function drawWeather() {} +//function modeWeather() {} // Start time on twist Bangle.on('twist', function() { @@ -223,9 +223,8 @@ function fixTime() { Bangle.on("GPS",function cb(g) { Bangle.setGPSPower(0,"time"); Bangle.removeListener("GPS",cb); - if (!g.time || (g.time.getFullYear()<2000) || - (g.time.getFullYear()>2200)) { - } else { + if (g.time && (g.time.getFullYear()>=2000) && + (g.time.getFullYear()<=2200)) { // We have a GPS time. Set time setTime(g.time.getTime()/1000); } diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js index 8a315118f..9d1bb26c8 100644 --- a/apps/doztime/app-bangle2.js +++ b/apps/doztime/app-bangle2.js @@ -16,11 +16,12 @@ const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; const timeColour = "#ffffff"; const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"]; const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line -const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line +const calen7 = {"size":26,"pt0":[42-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 const baseYear = 11584; const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let calenDef = calen10; let accum = new Date(baseDate.getTime()); let sequence = []; let timeActiveUntil; @@ -125,7 +126,7 @@ function formatDate(res,dateFormat){ } function writeDozTime(text,def){ - let pts = def.pts; + //let pts = def.pts; let x=def.pt0[0]; let y=def.pt0[1]; g_t.clear(); @@ -139,9 +140,9 @@ function writeDozTime(text,def){ } } function writeDozDate(text,def,colour){ - + dateColour = colour; - let pts = def.pts; + //let pts = def.pts; let x=def.pt0[0]; let y=def.pt0[1]; g_d.clear(); @@ -169,20 +170,22 @@ function drawTime() let date = ""; let timeDef; let x = 0; + let time; + let wait; dt.setDate(dt.getDate()); if(addTimeDigit){ x = 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); let msg = "00000"+Math.floor(x).toString(12); - let time = msg.substr(-5,3)+"."+msg.substr(-2); - let wait = 347*(1-(x%1)); + time = msg.substr(-5,3)+"."+msg.substr(-2); //TODO: should `time` and `wait` have been defined outside the if block? + wait = 347*(1-(x%1)); timeDef = time6; } else { x = 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); let msg = "0000"+Math.floor(x).toString(12); - let time = msg.substr(-4,3)+"."+msg.substr(-1); - let wait = 4167*(1-(x%1)); + time = msg.substr(-4,3)+"."+msg.substr(-1); + wait = 4167*(1-(x%1)); timeDef = time5; } if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day diff --git a/apps/doztime/metadata.json b/apps/doztime/metadata.json index a05bf1470..407b474ca 100644 --- a/apps/doztime/metadata.json +++ b/apps/doztime/metadata.json @@ -2,7 +2,7 @@ "id": "doztime", "name": "Dozenal Digital Time", "shortName": "Dozenal Digital", - "version": "0.05", + "version": "0.07", "description": "A dozenal Holocene calendar and dozenal diurnal digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/drained/ChangeLog b/apps/drained/ChangeLog index 8d196b10d..65c93e70f 100644 --- a/apps/drained/ChangeLog +++ b/apps/drained/ChangeLog @@ -3,3 +3,4 @@ 0.03: Permit exceptions to load in low-power mode, e.g. daylight saving time. Also avoid polluting global scope. 0.04: Enhance menu: enable bluetooth, visit settings & visit recovery +0.05: Enhance menu: permit toggling bluetooth diff --git a/apps/drained/app.js b/apps/drained/app.js index 37cc8c71d..c74affea3 100644 --- a/apps/drained/app.js +++ b/apps/drained/app.js @@ -53,6 +53,21 @@ var draw = function () { }, 60000 - (date.getTime() % 60000)); }; var reload = function () { + var showMenu = function () { + var menu = { + "Restore to full power": drainedRestore, + }; + if (NRF.getSecurityStatus().advertising) + menu["Disable BLE"] = function () { NRF.sleep(); showMenu(); }; + else + menu["Enable BLE"] = function () { NRF.wake(); showMenu(); }; + menu["Settings"] = function () { return load("setting.app.js"); }; + menu["Recovery"] = function () { return Bangle.showRecoveryMenu(); }; + menu["Exit menu"] = reload; + if (nextDraw) + clearTimeout(nextDraw); + E.showMenu(menu); + }; Bangle.setUI({ mode: "custom", remove: function () { @@ -60,15 +75,7 @@ var reload = function () { clearTimeout(nextDraw); nextDraw = undefined; }, - btn: function () { - var menu = { - "Restore to full power": drainedRestore, - "Enable BLE": function () { return NRF.wake(); }, - "Settings": function () { return load("setting.app.js"); }, - "Recovery": function () { return Bangle.showRecoveryMenu(); }, - }; - E.showMenu(menu); - } + btn: showMenu }); Bangle.CLOCK = 1; g.clear(); diff --git a/apps/drained/app.ts b/apps/drained/app.ts index ed40262fa..de6114f99 100644 --- a/apps/drained/app.ts +++ b/apps/drained/app.ts @@ -1,7 +1,7 @@ const app = "drained"; // from boot.js -declare var drainedInterval: IntervalId | undefined; +declare let drainedInterval: IntervalId | undefined; if(typeof drainedInterval !== "undefined") drainedInterval = clearInterval(drainedInterval) as undefined; @@ -72,21 +72,31 @@ const draw = () => { }; const reload = () => { + const showMenu = () => { + const menu: { [k: string]: () => void } = { + "Restore to full power": drainedRestore, + }; + + if (NRF.getSecurityStatus().advertising) + menu["Disable BLE"] = () => { NRF.sleep(); showMenu(); }; + else + menu["Enable BLE"] = () => { NRF.wake(); showMenu(); }; + + menu["Settings"] = () => load("setting.app.js"); + menu["Recovery"] = () => Bangle.showRecoveryMenu(); + menu["Exit menu"] = reload; + + if(nextDraw) clearTimeout(nextDraw); + E.showMenu(menu); + }; + Bangle.setUI({ mode: "custom", remove: () => { if (nextDraw) clearTimeout(nextDraw); nextDraw = undefined; }, - btn: () => { - const menu = { - "Restore to full power": drainedRestore, - "Enable BLE": () => NRF.wake(), - "Settings": () => load("setting.app.js"), - "Recovery": () => Bangle.showRecoveryMenu(), - }; - E.showMenu(menu); - } + btn: showMenu }); Bangle.CLOCK=1; diff --git a/apps/drained/metadata.json b/apps/drained/metadata.json index ea18f6fcf..115289d09 100644 --- a/apps/drained/metadata.json +++ b/apps/drained/metadata.json @@ -1,7 +1,7 @@ { "id": "drained", "name": "Drained", - "version": "0.04", + "version": "0.05", "description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals", "readme": "README.md", "icon": "icon.png", diff --git a/apps/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog index d8d174c4c..0541d11de 100644 --- a/apps/drinkcounter/ChangeLog +++ b/apps/drinkcounter/ChangeLog @@ -1,4 +1,5 @@ 0.10: Initial release - still work in progress 0.15: Added settings and calculations 0.20: Added status saving -0.25: Adopted for Bangle.js 1 - kind of \ No newline at end of file +0.25: Adopted for Bangle.js 1 - kind of +0.26: Minor code improvements diff --git a/apps/drinkcounter/app.js b/apps/drinkcounter/app.js index 323d9fb41..b231930d7 100644 --- a/apps/drinkcounter/app.js +++ b/apps/drinkcounter/app.js @@ -22,7 +22,7 @@ const maxDrinks = 2; // 3 drinks var firstDrinkTime = null; var firstDrinkTimeTime = null; -var confBeerSize; +//var confBeerSize; var confSex; var confWeight; var confWeightUnit; @@ -97,7 +97,7 @@ function loadMySettings() { function def (value, def) {return value !== undefined ? value : def;} var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - confBeerSize = def(settings.beerSize, "0.3L"); + //confBeerSize = def(settings.beerSize, "0.3L"); confSex = def(settings.sex, "male"); confWeight = def(settings.weight, 80); confWeightUnit = def(settings.weightUnit, "Kilo"); diff --git a/apps/drinkcounter/metadata.json b/apps/drinkcounter/metadata.json index 2b8d7fe71..315a5845b 100644 --- a/apps/drinkcounter/metadata.json +++ b/apps/drinkcounter/metadata.json @@ -2,7 +2,7 @@ "id": "drinkcounter", "name": "Drink Counter", "shortName": "Drink Counter", - "version": "0.25", + "version": "0.26", "description": "Counts drinks you had for science. Calculates blood alcohol content (BAC)", "allow_emulator":true, "icon": "drinkcounter.png", diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 6c096f45b..5cac5770e 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -29,3 +29,4 @@ immediately follows the correct theme. when moving pages. Add caching for faster startups. 0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions 0.24: Add buzz-on-interaction setting +0.25: Minor code improvements diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index a3ddd2538..2108910fc 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -42,7 +42,7 @@ let Npages = Math.ceil(Napps/4); let maxPage = Npages-1; let selected = -1; - let oldselected = -1; + //let oldselected = -1; let page = 0; const XOFF = 24; const YOFF = 30; @@ -104,7 +104,7 @@ let swipeListenerDt = function(dirLeftRight, dirUpDown){ updateTimeoutToClock(); selected = -1; - oldselected=-1; + //oldselected=-1; if(settings.swipeExit && dirLeftRight==1) Bangle.showClock(); if (dirUpDown==-1||dirLeftRight==-1){ ++page; if (page>maxPage) page=0; diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 5e25b61fb..bac0ed369 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.24", + "version": "0.25", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/dwm-clock/ChangeLog b/apps/dwm-clock/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/dwm-clock/ChangeLog +++ b/apps/dwm-clock/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/dwm-clock/app.js b/apps/dwm-clock/app.js index 773777ca5..6d9bd3767 100644 --- a/apps/dwm-clock/app.js +++ b/apps/dwm-clock/app.js @@ -143,7 +143,7 @@ function renderScreen() { } function renderAndQueue() { - timeoutID = setTimeout(renderAndQueue, 60000 - (Date.now() % 60000)); + /*timeoutID =*/ setTimeout(renderAndQueue, 60000 - (Date.now() % 60000)); renderScreen(); } @@ -162,7 +162,7 @@ var now = new Date(); var defLonOffset = getLongitudeOffset().lon; var lonOffset = defLonOffset; -var timeoutID; +//var timeoutID; var timeoutIDTouch; Bangle.on('drag', function(touch) { diff --git a/apps/dwm-clock/metadata.json b/apps/dwm-clock/metadata.json index 677c78f17..2a03c396c 100644 --- a/apps/dwm-clock/metadata.json +++ b/apps/dwm-clock/metadata.json @@ -2,7 +2,7 @@ "id": "dwm-clock", "name": "Daylight World Map Clock", "shortName": "DWM Clock", - "version": "0.01", + "version": "0.02", "description": "A clock with a daylight world map", "readme":"README.md", "icon": "app.png", diff --git a/apps/edisonsball/ChangeLog b/apps/edisonsball/ChangeLog index b71b8bb0d..c871dbe41 100644 --- a/apps/edisonsball/ChangeLog +++ b/apps/edisonsball/ChangeLog @@ -1,2 +1,4 @@ 0.01: Initial version 0.02: Added BangleJS Two +0.03: Minor code improvements +0.04: Minor code improvements diff --git a/apps/edisonsball/app.js b/apps/edisonsball/app.js index 2aa317829..39b764dfe 100644 --- a/apps/edisonsball/app.js +++ b/apps/edisonsball/app.js @@ -104,10 +104,10 @@ function getStandardDeviation (array) { } function checkHR() { - var bpm = currentBPM, isCurrent = true; + var bpm = currentBPM; //isCurrent = true; if (bpm===undefined) { bpm = lastBPM; - isCurrent = false; + //isCurrent = false; } if (bpm===undefined || bpm < lower_limit_BPM || bpm > upper_limit_BPM) bpm = "--"; @@ -118,8 +118,8 @@ function checkHR() { if(HR_samples.length == 5){ g.clear(); - average_HR = average(HR_samples).toFixed(0); - stdev_HR = getStandardDeviation (HR_samples).toFixed(1); + let average_HR = average(HR_samples).toFixed(0); + let stdev_HR = getStandardDeviation (HR_samples).toFixed(1); if (ISBANGLEJS1) { g.drawString("HR: " + average_HR, 120,100); diff --git a/apps/edisonsball/metadata.json b/apps/edisonsball/metadata.json index dfeb4451e..8526c7926 100644 --- a/apps/edisonsball/metadata.json +++ b/apps/edisonsball/metadata.json @@ -2,7 +2,7 @@ "id": "edisonsball", "name": "Edison's Ball", "shortName": "Edison's Ball", - "version": "0.02", + "version": "0.04", "description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness", "icon": "app-icon.png", "tags": "sleep,hyponagogia,quick,nap", 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/encourageclk/ChangeLog b/apps/encourageclk/ChangeLog index 83c07f784..9cb404008 100644 --- a/apps/encourageclk/ChangeLog +++ b/apps/encourageclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New face :) 0.02: code improvements 0.03: code improvments to queuedraw and draw +0.04: Minor code improvements diff --git a/apps/encourageclk/app.js b/apps/encourageclk/app.js index a78a788ba..1f19cc314 100644 --- a/apps/encourageclk/app.js +++ b/apps/encourageclk/app.js @@ -3,7 +3,6 @@ require("FontHaxorNarrow7x17").add(Graphics); require("FontDylex7x13").add(Graphics); -const storage = require('Storage'); const locale = require("locale"); const dateutil = require("date_utils"); const currentFont=g.getFont(); @@ -52,7 +51,7 @@ function queueDraw() { function draw() { var time = locale.time(d, 1); - var date = locale.date(d); + //var date = locale.date(d); var mo = dateutil.month(d.getMonth() + 1, 1); g.drawImage(bgimg,0,offset); //bg diff --git a/apps/encourageclk/metadata.json b/apps/encourageclk/metadata.json index 4e5d630cf..f3816c9de 100644 --- a/apps/encourageclk/metadata.json +++ b/apps/encourageclk/metadata.json @@ -2,7 +2,7 @@ "id": "encourageclk", "name": "Encouragement & Positivity Clock", "shortName":"Encouragement Clock", - "version": "0.03", + "version": "0.04", "description": "Tap on the watch for a note of encouragement", "icon": "app.png", "type": "clock", diff --git a/apps/espruinoterm/ChangeLog b/apps/espruinoterm/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/espruinoterm/ChangeLog +++ b/apps/espruinoterm/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/espruinoterm/app.js b/apps/espruinoterm/app.js index 348190db4..1253b253a 100644 --- a/apps/espruinoterm/app.js +++ b/apps/espruinoterm/app.js @@ -7,7 +7,7 @@ var R = Bangle.appRect; var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); var termVisible = false; termg.setFont("6x8"); -term = require("VT100").connect(termg, { +let term = require("VT100").connect(termg, { charWidth : 6, charHeight : 8 }); diff --git a/apps/espruinoterm/metadata.json b/apps/espruinoterm/metadata.json index 25e6183e1..d967e0e1a 100644 --- a/apps/espruinoterm/metadata.json +++ b/apps/espruinoterm/metadata.json @@ -2,7 +2,7 @@ "id": "espruinoterm", "name": "Espruino Terminal", "shortName": "Espruino Term", - "version": "0.01", + "version": "0.02", "description": "Send commands to other Espruino devices via the Bluetooth UART interface, and see the result on a VT100 terminal. Customisable commands!", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog index b5a33bd2e..8aed5d989 100644 --- a/apps/f9lander/ChangeLog +++ b/apps/f9lander/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Add lightning 0.03: Convert Yes/No On/Off in settings to checkboxes +0.04: Minor code improvements diff --git a/apps/f9lander/app.js b/apps/f9lander/app.js index 2f17a5bd5..d195a7c67 100644 --- a/apps/f9lander/app.js +++ b/apps/f9lander/app.js @@ -45,7 +45,7 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2, var exploded = false; var nExplosions = 0; -var landed = false; +//var landed = false; var lightning = 0; var settings = require("Storage").readJSON('f9settings.json', 1) || {}; diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index 5a3887c9e..868b70f71 100644 --- a/apps/f9lander/metadata.json +++ b/apps/f9lander/metadata.json @@ -1,7 +1,7 @@ { "id": "f9lander", "name": "Falcon9 Lander", "shortName":"F9lander", - "version":"0.03", + "version": "0.04", "description": "Land a rocket booster", "icon": "f9lander.png", "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], diff --git a/apps/fallout_clock/.gitignore b/apps/fallout_clock/.gitignore new file mode 100644 index 000000000..e5f9ba937 --- /dev/null +++ b/apps/fallout_clock/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +res/ + +fallout_clock.code-workspace + +package.json +package-lock.json diff --git a/apps/fallout_clock/ChangeLog b/apps/fallout_clock/ChangeLog new file mode 100644 index 000000000..ee9876b1a --- /dev/null +++ b/apps/fallout_clock/ChangeLog @@ -0,0 +1,5 @@ +0.10: (20240125) Basic Working Clock. +0.11: (20240125) Widgets Added. Improved Interval Loop. +0.12: (20240221) Fix: Month Reporting Wrong. +0.20: (20240223) Created as a Package. +0.21: (20240223) Added StandardJS and NPM. diff --git a/apps/fallout_clock/LICENSE b/apps/fallout_clock/LICENSE new file mode 100644 index 000000000..d9d472761 --- /dev/null +++ b/apps/fallout_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Zachary D. Skelton + +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. diff --git a/apps/fallout_clock/README.md b/apps/fallout_clock/README.md new file mode 100644 index 000000000..b48e7e762 --- /dev/null +++ b/apps/fallout_clock/README.md @@ -0,0 +1,29 @@ +# Fallout Clock + +Inspired by the aesthetic of the Fallout series, this clock face looks to emulate the color and feel of a PipBoy. + +![clockface](./res/screenshot.png) + +## Usage + +You can also go into Settings, and choose it as the default clock under **Select Clock**. + +## Planned Features: +- Display Steps as Health +- Display Heartrate +- Brighter Color when the backlight is not on. +- Configurable Settings + +## Controls + +Zero Settings, Zero Configuration. Install and add as your clockface. + +## Requests + +To request new features, add [an issue](https://github.com/zskelton/fallout_clock/issues). + +## Creator + +Zachary D. Skelton \ +[Skelton Networks](https://skeltonnetworks.com)\ +[Github](https://github.com/zskelton) \ No newline at end of file diff --git a/apps/fallout_clock/app-icon.js b/apps/fallout_clock/app-icon.js new file mode 100644 index 000000000..c84c6fb48 --- /dev/null +++ b/apps/fallout_clock/app-icon.js @@ -0,0 +1 @@ +atob("MDDDAb88//9u/1r/1/YZrgAAit4kkkkkkkkkkAAVIkkkkkkkkkkkkkkkkkkAAAARJJIkkkkkkkkkkkkkkkAAAACJJJJUkkkkkkkkkkkkkAAAAARJJJJAAkkkkkkkkkkkAAAAACpJJJKgAAkkkkkkkkkgAAAAAVJJJJIAAAEkkkkkkkkAAAAACpJfpJUAAAAkkkkkkkgAAAAABJf/9JAAAAAEkkkkkkAAAAAARJdf/+gAAAAAkkkkkgAAAAAC//dL//gAAAAAEkkkkAAAAYADpJJL//8AAAAAAkkkkAAAD8AdJJJL///gAAAAAkkkgAAADr/pJJL////0AAAAAEkkgAAABJJL/pfb////gAAAAAkkAAAADpJeu22X////4AAAAAkkAAAADpL1tttuf/7f8AAAAAkgAAAAb/+ttttuSSS7/AAAAAEgAAAAdeyttttySSSb/gAAAAEgAAAC9eWtttuaySSf/2SSSSkgAAAVfxtttttyySX//9JJJQAAAAAJetttttttyST//9JJJUAAAABJeOaNyNutySW//9JJKgAAAARJdu6N1tvRySS3/JJJUAAAACJJVuVu1tzRyST2/JJKAAAAAVJL1ttyttuNuSWW7pJKgAAACpJLxtt6NtttuSS27pJUAAAAVJJLxtt6ttttuSWT9JKgAAAAJJJLxttzNtttuSST9JIAAAAiJJJL1ttttt2NuSSS9JUAAAA2222212xtty3RySSS9KgAAAEgAAAAZ6OW2tu1ySST9QAAAAEgAAAAaW1ttu2VySSXKAAAAAEkAAAACtu221ttySbdKgAAAAEkAAAADNty1ttuST9JUAAAAAkkAAAAAVty1ttuSXpKAAAAAAkkgAAAACtttttyT9JIAAAAAEkkkAAAAARttttyfdJUAAAAAEkkkAAAAACtttuSzJKgAAAAAkkkkgAAAAAWtuSSfpQAAAAAEkkkkkAAAAADa2yT9JAAAAAAkkkkkkgAAAAD7e3/pKgAAAAAkkkkkkkAAAAVL/9JJUAAAAAEkkkkkkkgAAARJJJJKAAAAAEkkkkkkkkkAAAJJJJJIAAAAAkkkkkkkkkkkACpJJJJUAAAAEkkkkkkkkkkkgCJJJJKgAAAEkkkkkkkkkkkkklJJJJQAAAEkkkkkkkkkkkkkkkkpJJAAEkkkkkkkkk=") diff --git a/apps/fallout_clock/clock.js b/apps/fallout_clock/clock.js new file mode 100644 index 000000000..56bb68a3a --- /dev/null +++ b/apps/fallout_clock/clock.js @@ -0,0 +1,141 @@ +/* global Bangle, Graphics, g */ + +// NAME: Fallout Clock (Bangle.js 2) +// DOCS: https://www.espruino.com/ReferenceBANGLEJS2 +// AUTHOR: Zachary D. Skelton +// VERSION: 0.1.0 (24JAN2024) - Creating [ Maj.Min.Bug ] REF: https://semver.org/ +// LICENSE: MIT License (2024) [ https://opensource.org/licenses/MIT ] + +/* THEME COLORS */ +// Dark Full - #000000 - (0,0.00,0) +// Dark Half - #002f00 - (0,0.18,0) +// Dark Zero - #005f00 - (0,0.37,0) +// Light Zero - #008e00 - (0,0.55,0) +// Light Half - #00bf00 - (0,0.75,0) +// Light Full - #00ee00 - (0,0.93,0) + +/* FONTS */ +// Font: Good Time Rg - https://www.dafont.com/good-times.font +// Large = 50px +Graphics.prototype.setLargeFont = function () { + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAABAAAAAAAB8AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAB8AAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAADwAAAAAAD8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAH/8AAAAAH/8AAAAAH/4AAAAAH/4AAAAAH/4AAAAAH/4AAAAAP/4AAAAAP/wAAAAAP/wAAAAAP/wAAAAAP/wAAAAAH/wAAAAAB/wAAAAAAfgAAAAAAHgAAAAAABgAAAAAAAAAAAAAAAAAAOAAAAAAB//AAAAAB//8AAAAB///wAAAA////AAAAf///4AAAP////AAAH////4AAD/+B//AAB/8AD/wAAf8AAP+AAP+AAB/gAD/AAAP8AA/gAAB/AAf4AAAf4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/AAAA/gAfwAAAP4AH8AAAD+AB/gAAB/gAP4AAAfwAD/AAAP8AA/4AAH+AAH/AAD/gAB/8AD/wAAP/4H/8AAB////+AAAP////AAAB////gAAAP///wAAAB///wAAAAH//wAAAAAf/wAAAAAAOAAAAAAAAAAAAAfgAAAAAAH8AAAAAAB/AAAAAAAfwAAAAAAH8AAAAAAB/AAAAAAAfwAAAAAAH+AAAAAAB/wAAAAAAf/////wAD/////8AA//////AAH/////wAA/////8AAH/////AAAf////wAAA////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAH8AA//8AB/AA///AAfwAf//wAH8AH//8AB/AD///AAfwA///wAH8Af//8AB/AH8B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwD+AfwAH+B/AH8AB///wB/AAf//8AfwAD///AH8AA///gB/AAH//wAfwAA//4AH8AAH/8AB/AAAf8AAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AB/AAAB/AAfwAAAfwAH8AAAH8AB/AAAB/AAfwAAAfwAH8AAAH8AB/AAAB/AAfwAAAfwAH8AfAH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8A/gH8AB/gP4D/AAf8H/A/wAH///8/8AA/////+AAP/////gAB/////4AAf/8//8AAD/+P/+AAAf/B//AAAA/AH/AAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///AAAAB///8AAAAf///gAAAH///8AAAB////gAAAf///4AAAH////AAAAAAD/wAAAAAAP8AAAAAAB/AAAAAAAfwAAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAH/////8AB//////AAf/////wAH/////8AB//////AAf/////wAH/////8AAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP4AAAAAAD+AAAAAAA/AAAAAAAAAAAAAAAAAAAAH///AD8AB///4B/AAf//+AfwAH///gH8AB///4B/AAf//+AfwAH///gH8AB///4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH8D/AAfwB/h/wAH8Af//8AB/AD//+AAfwA///gAH8AH//wAB/AA//4AAfgAH/8AAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAB//4AAAAB///wAAAB////AAAA////4AAAf////AAAP////4AAH/////AAB//fv/wAA/8H4f+AAP+B+D/gAH/AfgP8AB/gH4D/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+A/wAH8Afwf8AB/AH///AAfwA///gAH8AP//4AB/AD//8AAfwAf/+AAH8AD//AAAAAAP/gAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAB/AAAABAAfwAAAAwAH8AAAAcAB/AAAAfAAfwAAAPwAH8AAAH8AB/AAAD/AAfwAAD/wAH8AAB/8AB/AAA//AAfwAAf/gAH8AAP/gAB/AAP/wAAfwAH/4AAH8AD/8AAB/AB/8AAAfwB/+AAAH8A//AAAB/Af/gAAAfwP/gAAAH8P/wAAAB/H/4AAAAfz/8AAAAH9/8AAAAB//+AAAAAf//AAAAAH//gAAAAB//gAAAAAf/wAAAAAH/4AAAAAB/8AAAAAAf8AAAAAAH+AAAAAAB/AAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAD/gP/gAAB/+H/8AAA//z//gAAf////8AAP/////gAD/////4AB//////AAf+P/B/wAH+A/gP8AB/AP4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8AfgH8AB/AH4B/AAfwB+AfwAH8A/gH8AB/gP8D/AAf+P/h/wAH/////8AA/////+AAP/////gAB/////wAAP/8//4AAB/+H/8AAAH+A/+AAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAH/4AAAAAH//gAAAAD//8AAAAB///AD8AA///4B/AAP///AfwAH///wH8AB/4f8B/AAf4B/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH8APwH8AB/AD8B/AAfwA/AfwAH+APwP8AB/wD8D/AAP+A/B/gAD/wPx/4AAf/j9/+AAH/////AAA/////gAAH////4AAA////8AAAH///8AAAAf//+AAAAA//8AAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAEAAAAD4AHwAAAB/AD+AAAAfwA/gAAAH8AP4AAAB/AD+AAAAfwA/gAAAD4AHwAAAAMAAYAAAAAAAAAAAAAAAAA'), + 46, + atob('DRYqEykpKiwsJi0rDQ=='), + 50 | 65536 + ) + return this +} + +// Medium = 16px () +Graphics.prototype.setMediumFont = function () { + this.setFontCustom( + atob('AAAAAAAADwAAAB8AAAAPAAAAGwAAAb8AAB/9AAH/kAAv+QAA/4AAAPQAAAACvkAAH//wAD///AC/Qf4A/AA/APQAHwDwAA8A9AAfAPwAPwC+Qf4AP//8AB//9AAG/4AAoAAAAPAAAAD4AAAA////AL///wAv//8AAAAAAPAL/wDwH/8A8D//APA9DwDwPA8A8DwPAPA8DwDwPA8A9DwPAP78DwD/+A8AL+APAPAADwDwAA8A8BQPAPA8DwDwPA8A8DwPAPA8DwDwPA8A9DwfAP7/vwD///8AP+v8AAUBUACqqQAA//9AAP//wABVW8AAAAPAAAADwAAAA8AAAAPAAAADwAD///8A////AP///wAAA8AAAAKAAAAAAAD//A8A//wPAP/8DwDwPA8A8DwPAPA8DwDwPA8A8DwPAPA8DwDwPR8A8D+/APAv/gCgC/gAC//gAD///AC///4A/Tx/APg8LwDwPA8A8DwPAPA8DwDwPA8A8D0fAPA/vwDwL/4AoAv4AAAAQABQAAAA8AAHAPAAHwDwAL8A8AP/APAf+ADwf9AA8v9AAP/4AAD/4AAA/0AAAP0AAAAAAEAAL9v4AL///gD//78A+H0vAPA8DwDwPA8A8DwPAPA8DwDwPA8A9D0fAP7/vwD///8AP9v8AC/4AAC//g8A/r8PAPQfDwDwDw8A8A8PAPAPDwDwDw8A+A8fAP5PfwC///4AL//8AAb/4AAAAAAAAAAAAAA8DwAAfB8AADwPAA=='), + 46, + atob('BAcNBg0NDg4ODA4OBA=='), + 16 | 131072 + ) + return this +} + +/* VARIABLES */ +// Const +const H = g.getHeight() +const W = g.getWidth() +// Mutable +let timer = null + +/* UTILITY FUNCTIONS */ +// Return String of Current Time +function getCurrentTime () { + try { + const d = new Date() + const h = d.getHours() + const m = d.getMinutes() + return `${h}:${m.toString().padStart(2, 0)}` + } catch (e) { + console.log(e) + return '0:00' + } +} + +// Return String of Current Date +function getCurrentDate () { + try { + const d = new Date() + const year = d.getFullYear() + const month = d.getMonth() + const day = d.getDate() + const display = `${month + 1}.${day.toString().padStart(2, 0)}.${year}` + return display + } catch (e) { + console.log(e) + return '0.0.0000' + } +} + +// Set A New Draw for the Next Minute +function setNextDraw () { + console.log('tick') + // Clear Timeout + if (timer) { + clearInterval(timer) + } + // Calculate time until next minute + const d = new Date() + const s = d.getSeconds() + const ms = d.getMilliseconds() + const delay = 60000 - (s * 1000) - ms + // Set Timeout + timer = setInterval(draw, delay) +} + +function draw () { + // Reset Variables + g.reset() + // Set Background Color + g.setBgColor(0, 0, 0) + // Draw Background + g.setColor(0, 0, 0) + g.fillRect(0, 0, W, H) + // Set Font for Time + g.setColor(0, 0.93, 0) + g.setLargeFont() + g.setFontAlign(0, 0) + // Draw Time + const time = getCurrentTime() + g.drawString(time, W / 2, H / 2, true /* clear background */) + // Set Font for Date + g.setColor(0, 0.75, 0) + g.setMediumFont() + g.setFontAlign(0, 1) + // Draw Date + const dateStr = getCurrentDate() + g.drawString(dateStr, W / 2, H - 45, true) + // Draw Border + g.setColor(0, 0.93, 0) + g.drawLine(5, 36, W - 5, 36) + g.drawLine(5, H - 9, W - 5, H - 9) + g.setColor(0, 0.18, 0) + g.fillRect(0, 27, W, 32) + g.fillRect(0, H, W, H - 5) + // Draw Widgets + Bangle.drawWidgets() + // Schedule Next Draw + setNextDraw() +} + +/* MAIN LOOP */ +function main () { + // Clear Screen + g.clear() + // Set as Clock to Enable Launcher Screen on BTN1 + Bangle.setUI('clock') + // Load Widgets + Bangle.loadWidgets() + // Draw Clock + draw() +} + +/* BOOT CODE */ +main() diff --git a/apps/fallout_clock/icon.png b/apps/fallout_clock/icon.png new file mode 100644 index 000000000..fc9bc1fdc Binary files /dev/null and b/apps/fallout_clock/icon.png differ diff --git a/apps/fallout_clock/metadata.json b/apps/fallout_clock/metadata.json new file mode 100644 index 000000000..20861411a --- /dev/null +++ b/apps/fallout_clock/metadata.json @@ -0,0 +1,18 @@ +{ + "id":"fallout_clock", + "name":"Fallout Clock", + "version":"0.21", + "description":"A simple clock for the Fallout fan", + "icon":"icon.png", + "type":"clock", + "tags": "clock,fallout,green,retro", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fallout_clock.app.js", "url":"clock.js"}, + {"name":"fallout_clock.img", "url":"app-icon.js", "evaluate":true} + ], + "screenshots": [ + {"url":"./screenshot.png", "name":"Fallout Clock Screenshot"} + ] +} diff --git a/apps/fallout_clock/res/fallout_icon.png b/apps/fallout_clock/res/fallout_icon.png new file mode 100644 index 000000000..fc9bc1fdc Binary files /dev/null and b/apps/fallout_clock/res/fallout_icon.png differ diff --git a/apps/fallout_clock/res/good times rg.otf b/apps/fallout_clock/res/good times rg.otf new file mode 100644 index 000000000..53c181cca Binary files /dev/null and b/apps/fallout_clock/res/good times rg.otf differ diff --git a/apps/fallout_clock/res/screenshot.png b/apps/fallout_clock/res/screenshot.png new file mode 100644 index 000000000..253554b72 Binary files /dev/null and b/apps/fallout_clock/res/screenshot.png differ diff --git a/apps/fallout_clock/screenshot.png b/apps/fallout_clock/screenshot.png new file mode 100644 index 000000000..253554b72 Binary files /dev/null and b/apps/fallout_clock/screenshot.png differ diff --git a/apps/fastload/README.md b/apps/fastload/README.md index f2ff71e93..f7fab4933 100644 --- a/apps/fastload/README.md +++ b/apps/fastload/README.md @@ -1,11 +1,17 @@ +#### ⚠️EXPERIMENTAL⚠️ + # Fastload Utils -*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app. +Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app. This allows fast loading of all apps with two conditions: * Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded. * Current app can be removed completely from RAM. +#### ⚠️ KNOWN ISSUES ⚠️ + +* Fastload currently does not play nice with the automatic reload option of the apploader. App installs and upgrades are unreliable since the fastload causes code to run after reset and interfere with the upload process. + ## Settings * Activate app history and navigate back through recent apps instead of immediately loading the clock face diff --git a/apps/fastreset/README.md b/apps/fastreset/README.md index 44b855454..b23023f4a 100644 --- a/apps/fastreset/README.md +++ b/apps/fastreset/README.md @@ -1,6 +1,6 @@ # Fast Reset -Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. +Reset the watch to the clock face by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. Fast Reset was developed with the app history feature of 'Fastload Utils' in mind. If many apps are in the history stack, the user may want a fast way to exit directly to the clock face without using the firmwares reset function. diff --git a/apps/fastreset/metadata.json b/apps/fastreset/metadata.json index 455649b48..ccd5e1ce4 100644 --- a/apps/fastreset/metadata.json +++ b/apps/fastreset/metadata.json @@ -2,7 +2,7 @@ "name": "Fast Reset", "shortName":"Fast Reset", "version":"0.03", - "description": "Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", + "description": "Reset the watch to the clock face by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", "icon": "app.png", "type": "bootloader", "tags": "system", diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index 7e7307c59..35fa366a4 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1,3 +1,5 @@ 0.01: First published version of app 0.02: Move to Bangle.setUI to launcher support 0.03: Tell clock widgets to hide. +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js index 838a5578d..52607b9fc 100644 --- a/apps/fclock/fclock.app.js +++ b/apps/fclock/fclock.app.js @@ -2,7 +2,7 @@ var minutes; var seconds; var hours; var date; -var first = true; +//var first = true; var locale = require('locale'); var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; @@ -86,7 +86,7 @@ const drawSec = function (sections, color) { const drawClock = function () { - currentTime = new Date(); + const currentTime = new Date(); //Get date as a string date = dateStr(currentTime); @@ -163,12 +163,10 @@ const drawHR = function () { } if (grow) { - color = settings.hr.color; - g.setColor(color); + g.setColor(settings.hr.color); g.fillCircle(settings.hr.x, settings.hr.y, size); } else { - color = "#000000"; - g.setColor(color); + g.setColor("#000000"); g.drawCircle(settings.hr.x, settings.hr.y, size); } }; diff --git a/apps/fclock/metadata.json b/apps/fclock/metadata.json index dffb197a2..3491be0e2 100644 --- a/apps/fclock/metadata.json +++ b/apps/fclock/metadata.json @@ -2,7 +2,7 @@ "id": "fclock", "name": "fclock", "shortName": "F Clock", - "version": "0.03", + "version": "0.05", "description": "Simple design of a digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/ffcniftyapp/ChangeLog b/apps/ffcniftyapp/ChangeLog new file mode 100644 index 000000000..30dcec467 --- /dev/null +++ b/apps/ffcniftyapp/ChangeLog @@ -0,0 +1,3 @@ +0.01: New Clock Nifty A ++ >> adding more information on the right side of the clock + + diff --git a/apps/ffcniftyapp/README.md b/apps/ffcniftyapp/README.md new file mode 100644 index 000000000..d6795840e --- /dev/null +++ b/apps/ffcniftyapp/README.md @@ -0,0 +1,13 @@ +# Nifty-A ++ Clock + +This is the clock: + +![](screenshot_niftyapp.png) + +The week number (ISO8601) can be turned off in settings (default is `On`) +Weather and Steps can be also turned off in settings. + +![](screenshot_settings_niftyapp.png) + +Based on the # Nifty-A Clock by @alessandrococco + diff --git a/apps/ffcniftyapp/app-icon.js b/apps/ffcniftyapp/app-icon.js new file mode 100644 index 000000000..f0a2393b1 --- /dev/null +++ b/apps/ffcniftyapp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A5gX/+AGEn//mIWLgP/C4gGCAAMgC5UvC4sDC4YICkIhBgMQiEBE4Uxn4XDj//iEAn/yA4ICBgUikEikYXBBAIXEn/xJYURAYMygERkQHBiYLBKYIXF+AVDC4czgUSmIXBCQgED+ZeBR4YXBLYICDC5CPGC4IAIC40zmaPDC4MSLQQXK+ayCR4QXCiRoEC44ECh4bCC4MTiTDBC6ZHOC5B3NLYcvC4kBgL5BAAUikT+BfIIrB/8ykf/eYQXBkUTI4cBW4YQCgQGDmAXDkJfEC46GBAoJKCR4geCAAMRAAZRDAoIODO4UBPRIAJR5QXWgKNCTApNDC5Mv/6/DAwR3GAAyHCC4anJIo3/+bvEa4Uia4oXHkEvC4cvIgUf+YXKHYIvEAgcPC5QSGC5UBSwYXJLYQXFkUhgABBC5Ef/4mBl4XEmETmIXKgaXBmYCBC4cTkMxiQXJS4IACL4p3MgESCwJHFR5oxCiB3FkERC5cSToQXFmUyiAZFR48Bn7zCAQMjkfykQkBN4n/XgKPBAAQgCUQIfBUwYXHFgIGCdI4XDmYADmIIEkAWJAH4A4A==")) \ No newline at end of file diff --git a/apps/ffcniftyapp/app.js b/apps/ffcniftyapp/app.js new file mode 100644 index 000000000..840dd72ff --- /dev/null +++ b/apps/ffcniftyapp/app.js @@ -0,0 +1,174 @@ +const w = require("weather"); +//const locale = require("locale"); + +// Weather icons from https://icons8.com/icon/set/weather/color +function getSun() { + return require("heatshrink").decompress(atob("kEggILIgOAAZkDAYPAgeBwPAgIFBBgPhw4TBp/yAYMcnADBnEcAYMwhgDBsEGgE/AYP8AYYLDCYgbDEYYrD8fHIwI7CIYZLDL54AHA==")); +} +function getPartSun() { + return require("heatshrink").decompress(atob("kcjwIVSgOAAgUwAYUGAYVgBoQHBkAIBocIDIX4CIcOAYMYg/wgECgODgE8oFAmEDxEYgYZBgQLBGYNAg/ggcYgANBAIIxBsPAG4MYsAIBoQ3ChAQCgI4BHYUEBgUADIIPBh///4GBv//8Cda")); +} +//function getPartRain() { +// return require("heatshrink").decompress(atob("kEggIHEmADJjEwsEAjkw8EAh0B4EAg35wEAgP+CYMDwv8AYMDBAP2g8HgH+g0DBYMMgPwAYX8gOMEwMG3kAg8OvgSBjg2BgcYGQIcBAY5CBg0Av//HAM///4MYgNBEIMOCoUMDoUAnBwGkEA")); +//} +function getCloud() { + return require("heatshrink").decompress(atob("kEggIfcj+AAYM/8ADBuFwAYPAmADCCAMBwEf8ADBhFwg4aBnEPAYMYjAVBhgDDDoQDHCYc4jwDB+EP///FYIDBMTgA==")); +} +function getSnow() { + return require("heatshrink").decompress(atob("kEggITQj/AAYM98ADBsEwAYPAjADCj+AgOAj/gAYMIuEHwEAjEPAYQVChk4AYQhCAYcYBYQTDnEPgEB+EH///IAQACE4IAB8EICIPghwDB4EeBYNAjgDBg8EAYQYCg4bCgZuFA==")); +} +function getRain() { + return require("heatshrink").decompress(atob("kEggIPMh+AAYM/8ADBuFwAYPgmADB4EbAYOAj/ggOAhnwg4aBnAeCjEcCIMMjADCDoQDHjAPCnAXCuEP///8EDAYJECAAXBwkAgPDhwDBwUMgEEhkggEOjFgFgMQLYQAOA==")); +} +function getStorm() { + return require("heatshrink").decompress(atob("kcjwIROgfwAYMB44ICsEwAYMYgYQCgAICoEHCwMYgFDwEHCYfgEAMA4AIBmAXCgUGFIVAwADBhEQFIQtCGwNggPgjAVBngCBv8Oj+AgfjwYpCGAIABn4kBgOBBAVwjBHBD4IdBgYNBGwUAkCdbA=")); +} +// err icon - https://icons8.com/icons/set/error +function getErr() { + return require("heatshrink").decompress(atob("kEggILIgOAAYsD4ADBg/gAYMGsADBhkwAYsYjADCjgDBmEMAYNxxwDBsOGAYPBwYDEgOBwOAgYDB4EDHYPAgwDBsADDhgDBFIcwjAHBjE4AYMcmADBhhNCKIcG/4AGOw4A==")); +} +//function getDummy() { +// return require("heatshrink").decompress(atob("gMBwMAwA")); +//} + + + + +/** +Choose weather icon to display based on condition. +Based on function from the Bangle weather app so it should handle all of the conditions +sent from gadget bridge. +*/ +function chooseIcon(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm") || + condition.includes("squalls") || + condition.includes("tornado")) return getStorm; + else if (condition.includes("freezing") || condition.includes("snow") || + condition.includes("sleet")) { + return getSnow; + } + else if (condition.includes("drizzle") || + condition.includes("shower") || + condition.includes("rain")) return getRain; + else if (condition.includes("clear")) return getSun; + else if (condition.includes("clouds")) return getCloud; + else if (condition.includes("few clouds") || + condition.includes("scattered clouds") || + condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("overcast") || + condition.includes("partly cloudy") || + condition.includes("ash")) { + return getPartSun; + } else return getErr; +} +/*function condenseWeather(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm") || + condition.includes("squalls") || + condition.includes("tornado")) return "storm"; + if (condition.includes("freezing") || condition.includes("snow") || + condition.includes("sleet")) { + return "snow"; + } + if (condition.includes("drizzle") || + condition.includes("shower") || + condition.includes("rain")) return "rain"; + if (condition.includes("clear")) return "clear"; + if (condition.includes("clouds")) return "clouds"; + if (condition.includes("few clouds") || + condition.includes("scattered clouds") || + condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("overcast") || + condition.includes("partly cloudy") || + condition.includes("ash")) { + return "scattered"; + } else { return "N/A"; } + return "N/A"; +} +*/ +// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 +function ISO8601_week_no(date) { + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + return 1 + Math.ceil((firstThursday - tdt) / 604800000); +} + +function format(value) { + return ("0" + value).substr(-2); +} + +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + init: function () { + const appRect = Bangle.appRect; + + this.viewport = { + width: appRect.w, + height: appRect.h + }; + + this.center = { + x: this.viewport.width / 2, + y: Math.round((this.viewport.height / 2) + appRect.y) + }; + + this.scale = g.getWidth() / this.viewport.width; + this.centerTimeScaleX = this.center.x + 32 * this.scale; + this.centerDatesScaleX = this.center.x + 40 * this.scale; + }, + draw: function (date) { + const hour = date.getHours() - (this.is12Hour && date.getHours() > 12 ? 12 : 0); + const month = date.getMonth() + 1; + // const monthName = require("date_utils").month(month, 1); + // const dayName = require("date_utils").dow(date.getDay(), 1); + let steps = Bangle.getHealthStatus("day").steps; + let curr = (w.get() === undefined ? "no data" : w.get()); // Get weather from weather app. + //let cWea =(curr === "no data" ? "no data" : curr.txt); + let cTemp= (curr === "no data" ? 273 : curr.temp); + // const temp = locale.temp(curr.temp - 273.15).match(/^(\D*\d*)(.*)$/); + let w_icon = chooseIcon(curr.txt === undefined ? "no data" : curr.txt ); + //let w_icon = chooseIcon(curr.txt); + + g.setFontAlign(1, 0).setFont("Vector", 90 * this.scale); + g.drawString(format(hour), this.centerTimeScaleX, this.center.y - 31 * this.scale); + g.drawString(format(date.getMinutes()), this.centerTimeScaleX, this.center.y + 46 * this.scale); + + g.fillRect(this.center.x + 30 * this.scale, this.center.y - 72 * this.scale, this.center.x + 32 * this.scale, this.center.y + 74 * this.scale); + + g.setFontAlign(-1, 0).setFont("Vector", 16 * this.scale); + g.drawString(format(date.getDate()), this.centerDatesScaleX, this.center.y - 62 * this.scale); //26 + g.drawString("." + format(month) + ".", this.centerDatesScaleX + 20, this.center.y - 62 * this.scale); //44 + g.drawString(date.getFullYear(date), this.centerDatesScaleX, this.center.y - 44 * this.scale); //62 + if (this.showWeekNum) + g.drawString("CW" + format(ISO8601_week_no(date)), this.centerDatesScaleX, this.center.y + -26 * this.scale); //15 + // print(w_icon()); + if (this.showWeather) { + g.drawImage(w_icon(), this.centerDatesScaleX, this.center.y - 8 * this.scale); + // g.drawString(condenseWeather(curr.txt), this.centerDatesScaleX, this.center.y + 24 * this.scale); + g.drawString((cTemp === undefined ? 273 : cTemp ) - 273 + "°C", this.centerDatesScaleX, this.center.y + 44 * this.scale); //48 + + } + if (this.showSteps) + g.drawString(steps, this.centerDatesScaleX, this.center.y + 66 * this.scale); + + }, + settingsFile: "ffcniftyapp.json" +}); +clock.start(); diff --git a/apps/ffcniftyapp/app.png b/apps/ffcniftyapp/app.png new file mode 100644 index 000000000..1cd8a49b7 Binary files /dev/null and b/apps/ffcniftyapp/app.png differ diff --git a/apps/ffcniftyapp/metadata.json b/apps/ffcniftyapp/metadata.json new file mode 100644 index 000000000..bbe8e7e69 --- /dev/null +++ b/apps/ffcniftyapp/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "ffcniftyapp", + "name": "Nifty-A Clock ++", + "version": "0.01", + "description": "A nifty clock with time and date and more", + "dependencies": {"weather":"app"}, + "icon": "app.png", + "screenshots": [{"url":"screenshot_niftyapp.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"ffcniftyapp.app.js","url":"app.js"}, + {"name":"ffcniftyapp.img","url":"app-icon.js","evaluate":true}, + {"name":"ffcniftyapp.settings.js","url":"settings.js"} + ], + "data": [{"name":"ffcniftyapp.json"}] +} diff --git a/apps/ffcniftyapp/screenshot_niftyapp.png b/apps/ffcniftyapp/screenshot_niftyapp.png new file mode 100644 index 000000000..2428523d0 Binary files /dev/null and b/apps/ffcniftyapp/screenshot_niftyapp.png differ diff --git a/apps/ffcniftyapp/screenshot_settings_niftyapp.png b/apps/ffcniftyapp/screenshot_settings_niftyapp.png new file mode 100644 index 000000000..0bc9cc72c Binary files /dev/null and b/apps/ffcniftyapp/screenshot_settings_niftyapp.png differ diff --git a/apps/ffcniftyapp/settings.js b/apps/ffcniftyapp/settings.js new file mode 100644 index 000000000..a1f09e454 --- /dev/null +++ b/apps/ffcniftyapp/settings.js @@ -0,0 +1,35 @@ + +(function (back) { + var DEFAULTS = { + 'showWeekNum': false, + 'showWeather': false, + 'showSteps': false, + }; + let settings = require('Storage').readJSON("ffcniftyapp.json", 1) || DEFAULTS; + E.showMenu({ + + "": { "title": "Nifty-A Clock ++" }, + "< Back": () => back(), + /*LANG*/"Show Week Number": { + value: settings.showWeekNum, + onchange: v => { + settings.showWeekNum = v; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + }, + /*LANG*/"Show Weather": { + value: settings.showWeather, + onchange: w => { + settings.showWeather = w; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + }, + /*LANG*/"Show Steps": { + value: settings.showSteps, + onchange: z => { + settings.showSteps = z; + require("Storage").writeJSON("ffcniftyapp.json", settings); + } + } + }); +}) diff --git a/apps/fileman/ChangeLog b/apps/fileman/ChangeLog index f5af86229..cc1456b31 100644 --- a/apps/fileman/ChangeLog +++ b/apps/fileman/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app! 0.02: Improve handling of large amounts of files (fix #579) 0.03: Update RegExp use (Was using backreference instead of character code) + 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/fileman/metadata.json b/apps/fileman/metadata.json index f5589e396..52f2fd06d 100644 --- a/apps/fileman/metadata.json +++ b/apps/fileman/metadata.json @@ -7,6 +7,7 @@ "icon": "icons8-filing-cabinet-48.png", "tags": "tools", "supports": ["BANGLEJS","BANGLEJS2"], + "interface": "manage_files.html", "readme": "README.md", "storage": [ {"name":"fileman.app.js","url":"fileman.app.js"}, diff --git a/apps/flashcards/ChangeLog b/apps/flashcards/ChangeLog index 6f9ed7196..4c0434f5b 100644 --- a/apps/flashcards/ChangeLog +++ b/apps/flashcards/ChangeLog @@ -2,4 +2,5 @@ 1.10: Download cards data from Trello public board 1.20: Configuration instructions added and card layout optimized 1.30: Font size can be changed in Settings -1.31: Fix for fast-loading support \ No newline at end of file +1.31: Fix for fast-loading support +1.32: Minor code improvements diff --git a/apps/flashcards/app.js b/apps/flashcards/app.js index d2118f8cb..43dde213e 100644 --- a/apps/flashcards/app.js +++ b/apps/flashcards/app.js @@ -39,7 +39,7 @@ while (str.length > maxLength) { let found = false; // Inserts new line at first whitespace of the line - for (i = maxLength - 1; i > 0; i--) { + for (let i = maxLength - 1; i > 0; i--) { if (str.charAt(i)==' ') { res = res + [str.slice(0, i), "\n"].join(''); str = str.slice(i + 1); diff --git a/apps/flashcards/metadata.json b/apps/flashcards/metadata.json index 096e7e918..dee6a9e3a 100644 --- a/apps/flashcards/metadata.json +++ b/apps/flashcards/metadata.json @@ -2,7 +2,7 @@ "id": "flashcards", "name": "Flash Cards", "shortName": "Flash Cards", - "version": "1.31", + "version": "1.32", "description": "Flash cards based on public Trello board", "readme":"README.md", "screenshots" : [ { "url":"screenshot.png" }], diff --git a/apps/flightdash/ChangeLog b/apps/flightdash/ChangeLog index 971e5b97e..b0e36941a 100644 --- a/apps/flightdash/ChangeLog +++ b/apps/flightdash/ChangeLog @@ -1 +1,2 @@ 1.00: initial release +1.01: Minor code improvements diff --git a/apps/flightdash/flightdash.app.js b/apps/flightdash/flightdash.app.js index f612836c6..ac0146210 100644 --- a/apps/flightdash/flightdash.app.js +++ b/apps/flightdash/flightdash.app.js @@ -13,7 +13,7 @@ const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1) const APP_NAME = 'flightdash'; const horizontalCenter = g.getWidth() / 2; -const verticalCenter = g.getHeight() / 2; +//const verticalCenter = g.getHeight() / 2; const dataFontHeight = 22; const secondaryFontHeight = 18; @@ -23,7 +23,7 @@ const labelFontHeight = 12; //globals var settings = {}; -var updateInterval; +//var updateInterval; var speed = '-'; var speedPrev = -1; var track = '-'; var trackPrev = -1; diff --git a/apps/flightdash/flightdash.settings.js b/apps/flightdash/flightdash.settings.js index 6d7de1287..cd1ecdac6 100644 --- a/apps/flightdash/flightdash.settings.js +++ b/apps/flightdash/flightdash.settings.js @@ -316,7 +316,6 @@ }, 'Use Baro': { value: !!settings.useBaro, // !! converts undefined to false - format: v => v ? 'On' : 'Off', onchange: v => { settings.useBaro = v; writeSettings(); diff --git a/apps/flightdash/metadata.json b/apps/flightdash/metadata.json index 0bc23a192..23d7323cc 100644 --- a/apps/flightdash/metadata.json +++ b/apps/flightdash/metadata.json @@ -2,7 +2,7 @@ "id": "flightdash", "name": "Flight Dashboard", "shortName":"Flight-Dash", - "version":"1.00", + "version": "1.01", "description": "Basic flight and navigation instruments", "icon": "flightdash.png", "screenshots": [{ "url": "screenshot.png" }], diff --git a/apps/flow/ChangeLog b/apps/flow/ChangeLog new file mode 100644 index 000000000..62542be60 --- /dev/null +++ b/apps/flow/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Minor code improvements diff --git a/apps/flow/app.js b/apps/flow/app.js index 5f4da8f35..1a8095327 100644 --- a/apps/flow/app.js +++ b/apps/flow/app.js @@ -193,7 +193,7 @@ let intervalId; if (BTN.read()) { for (let i = 0; i < 10; i++) { - color = getColor(i); + const color = getColor(i); g.setColor(color[0], color[1], color[2]); g.fillRect((i / 10) * h, 0, ((i + 1) / 10) * h, h); } diff --git a/apps/flow/metadata.json b/apps/flow/metadata.json index cbb81082d..7ba8f1b4a 100644 --- a/apps/flow/metadata.json +++ b/apps/flow/metadata.json @@ -2,7 +2,7 @@ "id": "flow", "name": "FLOW", "shortName": "FLOW", - "version": "0.01", + "version": "0.02", "description": "A game where you have to help a flow avoid white obstacles thing by tapping! This is a demake of an app which I forgot the name of. Press BTN(1) to restart. See if you can get to 2500 score!", "icon": "app.png", "tags": "game", diff --git a/apps/folderlaunch/ChangeLog b/apps/folderlaunch/ChangeLog index 2670571d7..c92db0901 100644 --- a/apps/folderlaunch/ChangeLog +++ b/apps/folderlaunch/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app! 0.02: Handle files potentially not existing -0.03: Add setting to disable vibration \ No newline at end of file +0.03: Add setting to disable vibration +0.04: Minor code improvements diff --git a/apps/folderlaunch/README.md b/apps/folderlaunch/README.md index 2caac29c3..b142e6878 100644 --- a/apps/folderlaunch/README.md +++ b/apps/folderlaunch/README.md @@ -33,4 +33,8 @@ Swiping up and down will scroll. Swiping from the left, using the back button, o * Move app here: Display a list of apps. Selecting one moves it into the folder. * One menu entry for each subfolder, which opens the folder management menu for that subfolder. * View apps: Only present if this folder contains apps, Display a menu of all apps in the folder. This is for information only, tapping the apps does nothing. - * Delete folder: Only present if not viewing the root folder. Delete the current folder and move all apps into the parent folder. \ No newline at end of file + * Delete folder: Only present if not viewing the root folder. Delete the current folder and move all apps into the parent folder. + +## Web interface + +Limited editing is possible from the web interface. You can add folders and move applications between folders. \ No newline at end of file diff --git a/apps/folderlaunch/app.js b/apps/folderlaunch/app.js index 438b9e06d..1a66dffac 100644 --- a/apps/folderlaunch/app.js +++ b/apps/folderlaunch/app.js @@ -78,19 +78,21 @@ var text = void 0; var fontSize = void 0; switch (entry.type) { - case 'app': + case 'app': { var app_1 = storage_1.readJSON(entry.id + '.info', false); icon = storage_1.read(app_1.icon); text = app_1.name; empty = false; fontSize = config_1.display.font; break; - case 'folder': + } + case 'folder': { icon = FOLDER_ICON_1; text = entry.id; empty = false; fontSize = config_1.display.font ? config_1.display.font : 12; break; + } default: continue; } @@ -132,12 +134,13 @@ y = config_1.display.rows - 1; var entry = grid_1[x][y]; switch (entry.type) { - case "app": + case "app": { buzz_1(); var infoFile = storage_1.readJSON(entry.id + '.info', false); load(infoFile.src); break; - case "folder": + } + case "folder": { buzz_1(); resetTimeout_1(); page_1 = 0; @@ -145,9 +148,11 @@ folder_1 = getFolder_1(folderPath_1); render_1(); break; - default: + } + default: { resetTimeout_1(); break; + } } }; var page_1 = 0; diff --git a/apps/folderlaunch/app.ts b/apps/folderlaunch/app.ts index 993570e59..aff8e5c7e 100644 --- a/apps/folderlaunch/app.ts +++ b/apps/folderlaunch/app.ts @@ -114,19 +114,21 @@ // Get the icon and text, skip if the space is empty. Always draw text for folders even if disabled switch (entry.type) { - case 'app': + case 'app': { let app = storage.readJSON(entry.id + '.info', false) as AppInfo; icon = storage.read(app.icon!)!; text = app.name; empty = false; fontSize = config.display.font; break; - case 'folder': + } + case 'folder': { icon = FOLDER_ICON; text = entry.id; empty = false; fontSize = config.display.font ? config.display.font : 12; break; + } default: continue; } @@ -184,12 +186,13 @@ // Handle the grid cell let entry: GridEntry = grid[x]![y]!; switch (entry.type) { - case "app": + case "app": { buzz(); let infoFile = storage.readJSON(entry.id + '.info', false) as AppInfo; load(infoFile.src); break; - case "folder": + } + case "folder": { buzz(); resetTimeout(); page = 0; @@ -197,9 +200,11 @@ folder = getFolder(folderPath); render(); break; - default: + } + default: { resetTimeout(); break; + } } } diff --git a/apps/folderlaunch/configLoad.ts b/apps/folderlaunch/configLoad.ts index 37841cd36..56c398cdd 100644 --- a/apps/folderlaunch/configLoad.ts +++ b/apps/folderlaunch/configLoad.ts @@ -80,7 +80,7 @@ function cleanAndSave(config: Config): Config { let infoFileSorter = (a: string, b: string): number => { let aJson = storage.readJSON(a, false) as AppInfo; let bJson = storage.readJSON(b, false) as AppInfo; - var n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); + const n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!); if (n) return n; // do sortorder first if (aJson.name < bJson.name) return -1; if (aJson.name > bJson.name) return 1; diff --git a/apps/folderlaunch/interface.html b/apps/folderlaunch/interface.html new file mode 100644 index 000000000..24d3f448d --- /dev/null +++ b/apps/folderlaunch/interface.html @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + +

Folder Launcher

+
+
+ + + +
+
+
+ + + + + + + + + + +
AppFolderMove to
+
+

Add a new folder

+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/apps/folderlaunch/metadata.json b/apps/folderlaunch/metadata.json index 0cf83abb4..9853c7daf 100644 --- a/apps/folderlaunch/metadata.json +++ b/apps/folderlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "folderlaunch", "name": "Folder launcher", - "version": "0.03", + "version": "0.04", "description": "Launcher that allows you to put your apps into folders", "icon": "icon.png", "type": "launch", @@ -10,6 +10,7 @@ "BANGLEJS2" ], "readme": "README.md", + "interface": "interface.html", "storage": [ { "name": "folderlaunch.app.js", diff --git a/apps/folderlaunch/types.d.ts b/apps/folderlaunch/types.d.ts index 86c0465e5..a6f927cd0 100644 --- a/apps/folderlaunch/types.d.ts +++ b/apps/folderlaunch/types.d.ts @@ -11,7 +11,7 @@ type Config = { showClocks: boolean, // Whether clocks are shown showLaunchers: boolean, // Whether launchers are shown disableVibration: boolean, // Whether vibration is disabled - hidden: Array, // IDs of apps to explicitly hide + hidden: Array, // IDs of apps to explicitly hide display: { rows: number, // Display an X by X grid of apps icon: boolean, // Whether to show icons diff --git a/apps/followtherecipe/ChangeLog b/apps/followtherecipe/ChangeLog index 9ba6a29bd..68ca62642 100644 --- a/apps/followtherecipe/ChangeLog +++ b/apps/followtherecipe/ChangeLog @@ -1 +1,2 @@ -0.01: New App \ No newline at end of file +0.01: New App +0.02: Minor code improvements diff --git a/apps/followtherecipe/app.js b/apps/followtherecipe/app.js index 8238a6c07..056632e27 100644 --- a/apps/followtherecipe/app.js +++ b/apps/followtherecipe/app.js @@ -1,10 +1,7 @@ -const storage = require("Storage"); const settings = require("Storage").readJSON("followtherecipe.json"); -const locale = require('locale'); -var ENV = process.env; +//const locale = require('locale'); var W = g.getWidth(), H = g.getHeight(); var screen = 0; -var Layout = require("Layout"); let maxLenghtHorizontal = 16; let maxLenghtvertical = 6; diff --git a/apps/followtherecipe/metadata.json b/apps/followtherecipe/metadata.json index 8ffeac2d2..0c1de0817 100644 --- a/apps/followtherecipe/metadata.json +++ b/apps/followtherecipe/metadata.json @@ -3,7 +3,7 @@ "name": "Follow The Recipe", "shortName":"FTR", "icon": "icon.png", - "version": "0.01", + "version": "0.02", "description": "Follow The Recipe (FTR) is a bangle.js app to follow a recipe step by step", "type": "app", "tags": "tool, tools, cook", diff --git a/apps/fontall/ChangeLog b/apps/fontall/ChangeLog new file mode 100644 index 000000000..1ba08319d --- /dev/null +++ b/apps/fontall/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Corrected formatting of punctuation \ No newline at end of file diff --git a/apps/fontall/README.md b/apps/fontall/README.md new file mode 100644 index 000000000..9b53058d9 --- /dev/null +++ b/apps/fontall/README.md @@ -0,0 +1,22 @@ +# Fonts (all languages) + +This library provides an international font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +All characters from Unicode codepoint 32 up until codepoint 65535 (U+FFFF) are included here, +which should be enough for most languages. + +**The font is 2MB and takes a while to upload** - if you don't require all the languages +it provides, consider installing another Font library like [extended fonts](https://banglejs.com/apps/?id=fontsext) +that contains just the characters you need instead. + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating font.pbf + +* Go to `bin` directory +* Run `./font_creator.js "All" ../apps/fontall/font.pbf` \ No newline at end of file diff --git a/apps/fontall/app.png b/apps/fontall/app.png new file mode 100644 index 000000000..e029647b7 Binary files /dev/null and b/apps/fontall/app.png differ diff --git a/apps/fontall/boot.js b/apps/fontall/boot.js new file mode 100644 index 000000000..07b99570e --- /dev/null +++ b/apps/fontall/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontall.pbf")); }; \ No newline at end of file diff --git a/apps/fontall/font.pbf b/apps/fontall/font.pbf new file mode 100644 index 000000000..02345589e Binary files /dev/null and b/apps/fontall/font.pbf differ diff --git a/apps/fontall/lib.js b/apps/fontall/lib.js new file mode 100644 index 000000000..8d5caf366 --- /dev/null +++ b/apps/fontall/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/fontall/metadata.json b/apps/fontall/metadata.json new file mode 100644 index 000000000..9f9683714 --- /dev/null +++ b/apps/fontall/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fontall", + "name": "Fonts (all languages)", + "version":"0.02", + "description": "Installs a font containing over 50,000 Unifont characters for Chinese, Japanese, Korean, Russian, and more. **Requires 2MB 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":"fontall.boot.js","url":"boot.js"}, + {"name":"fontall.pbf","url":"font.pbf"} + ] +} diff --git a/apps/fontext/ChangeLog b/apps/fontext/ChangeLog new file mode 100644 index 000000000..1ba08319d --- /dev/null +++ b/apps/fontext/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Corrected formatting of punctuation \ No newline at end of file diff --git a/apps/fontext/README.md b/apps/fontext/README.md new file mode 100644 index 000000000..f8f99f3a4 --- /dev/null +++ b/apps/fontext/README.md @@ -0,0 +1,25 @@ +# Fonts (extended) + +This library provides an international font that can be used to display messages. + +The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html). +All characters from Unicode codepoint 32 up until codepoint 1103 (U+044F) are included here, +which should be enough for [around 90% of languages](https://arxiv.org/pdf/1801.07779.pdf#page=5) +but **not** Chinese/Japanese/Korean. + +The font is 20kb so is far more sensible than the [2MB all regions](https://banglejs.com/apps/?id=fontsall) font +if you don't require non-latin languages. + + +https://arxiv.org/pdf/1801.07779.pdf#page=5 + +## Usage + +See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference) +for more information on fonts. + + +## Recreating font.pbf + +* Go to `bin` directory +* Run `./font_creator.js "Extended" ../apps/fontext/font.pbf` \ No newline at end of file diff --git a/apps/fontext/app.png b/apps/fontext/app.png new file mode 100644 index 000000000..e029647b7 Binary files /dev/null and b/apps/fontext/app.png differ diff --git a/apps/fontext/boot.js b/apps/fontext/boot.js new file mode 100644 index 000000000..e52483e16 --- /dev/null +++ b/apps/fontext/boot.js @@ -0,0 +1 @@ +Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontext.pbf")); }; \ No newline at end of file diff --git a/apps/fontext/font.pbf b/apps/fontext/font.pbf new file mode 100644 index 000000000..7abe4236d Binary files /dev/null and b/apps/fontext/font.pbf differ diff --git a/apps/fontext/lib.js b/apps/fontext/lib.js new file mode 100644 index 000000000..8d5caf366 --- /dev/null +++ b/apps/fontext/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/fontext/metadata.json b/apps/fontext/metadata.json new file mode 100644 index 000000000..7088fa3cc --- /dev/null +++ b/apps/fontext/metadata.json @@ -0,0 +1,17 @@ +{ "id": "fontext", + "name": "Fonts (150+ languages)", + "version":"0.02", + "description": "Installs a font containing 1000 Unifont characters, which should handle the majority of non-Chinese/Japanese/Korean languages (only 20kb)", + "icon": "app.png", + "tags": "font,fonts,language", + "type": "module", + "provides_modules" : ["font"], + "default" : true, + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"font","url":"lib.js"}, + {"name":"fontext.boot.js","url":"boot.js"}, + {"name":"fontext.pbf","url":"font.pbf"} + ] +} 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/forge/ChangeLog b/apps/forge/ChangeLog index 0c651c90b..ade90fc36 100644 --- a/apps/forge/ChangeLog +++ b/apps/forge/ChangeLog @@ -1,2 +1,3 @@ 0.01: attempt to import 0.02: Make it possible for Fastload Utils to fastload into this app. +0.03: Minor code improvements diff --git a/apps/forge/forge.app.js b/apps/forge/forge.app.js index b179fb540..5169a04e9 100644 --- a/apps/forge/forge.app.js +++ b/apps/forge/forge.app.js @@ -2,9 +2,9 @@ "Bangle.loadWidgets()"; // Facilitates fastloading to this app via Fastload Utils, while still not loading widgets on standard `load` calls. -st = require('Storage'); +const st = require('Storage'); -l = /^a\..*\.js$/; +let l = /^a\..*\.js$/; //l = /.*\.js/; l = st.list(l, {sf:false}); diff --git a/apps/forge/metadata.json b/apps/forge/metadata.json index 6e13a4df3..43baca169 100644 --- a/apps/forge/metadata.json +++ b/apps/forge/metadata.json @@ -1,6 +1,6 @@ { "id": "forge", "name": "App Forge", - "version":"0.02", + "version": "0.03", "description": "Easy way to run development versions of your apps", "icon": "app.png", "readme": "README.md", diff --git a/apps/ftclock/mkFourTwentyTz.js b/apps/ftclock/mkFourTwentyTz.js index 4e7829aa3..7c6d25995 100644 --- a/apps/ftclock/mkFourTwentyTz.js +++ b/apps/ftclock/mkFourTwentyTz.js @@ -1,3 +1,5 @@ +/* This file is designed to be run on the desktop, not Bangle.js */ +/* eslint-env node */ let fs = require('fs'); let csv = require('csv'); @@ -30,7 +32,7 @@ fs.createReadStream(__dirname+'/country.csv') } else { country = countries[r[1]]; // e.g. United States } - zone = zones[r[0]] || { "name": `${city}, ${country}` }; + let zone = zones[r[0]] || { "name": `${city}, ${country}` }; let starttime = parseInt(r[3] || "0"), // Bugger. They're feeding us blanks for UTC now offs = parseInt(r[4]); if (offs<0) { @@ -43,15 +45,15 @@ fs.createReadStream(__dirname+'/country.csv') zones[r[0]] = zone; }) .on('end', () => { - for (z in zones) { - zone = zones[z]; + for (let z in zones) { + let zone = zones[z]; if (zone.offs%60) continue; // One a dem funky timezones. Ignore. - zonelist = offsdict[zone.offs] || []; + let zonelist = offsdict[zone.offs] || []; zonelist.push(zone.name); offsdict[zone.offs] = zonelist; } - offsets = []; - for (o in offsdict) { + let offsets = []; + for (let o in offsdict) { offsets.unshift(parseInt(o)); } fs.open("fourTwentyTz.js","w", (err, fd) => { @@ -67,7 +69,7 @@ fs.createReadStream(__dirname+'/country.csv') fs.write(fd, ";\n", handleWrite); fs.write(fd, "exports.timezones = function(offs) {\n", handleWrite); fs.write(fd, " switch (offs) {\n", handleWrite); - for (i=0; i=R.w) { - slideX=R.w; - stop = true; + let draw = function() { + time_string = getTimeString(new Date()).replace('*', ''); + //print(time_string); + if (time_string != time_string_old) { + g.setFont('Vector', R.h/text_scale).setFontAlign(0, 0); + if (settings.animate) { + animate(3); + } else { + quickDraw(); + } } - //draw shifted new time - g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2); - g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2); - if (stop) { - time_string_old = time_string; - clearInterval(animInterval); - animInterval=undefined; - time_string_old_wrapped = time_string_new_wrapped; - } - print(Math.round((getTime() - time_start)*1000)) - }, 30); -}; + queueDraw(timeout); + }; -g.clear(); -loadSettings(); - -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ - if (on) { - draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - } -}); - -Bangle.setUI({ - mode : 'clock', - remove : function() { - // Called to unload all of the clock app - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; + let animate = function(step) { if (animInterval) clearInterval(animInterval); - animInterval = undefined; - require('widget_utils').show(); // re-show widgets + let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n"); + let slideX = 0; + //don't let pulling the drawer change y + let text_y = R.y + R.h/2; + animInterval = setInterval(function() { + //blank old time + g.setColor(g.theme.bg); + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, text_y); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, text_y); + g.setColor(g.theme.fg); + slideX += step; + let stop = false; + if (slideX>=R.w) { + slideX=R.w; + stop = true; + } + //draw shifted new time + g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, text_y); + g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, text_y); + if (stop) { + time_string_old = time_string; + clearInterval(animInterval); + animInterval=undefined; + time_string_old_wrapped = time_string_new_wrapped; + } + //print(Math.round((getTime() - time_start)*1000)); + }, 30); + }; + + let quickDraw = function() { + let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n"); + g.setColor(g.theme.bg); + g.drawString(time_string_old_wrapped, R.x + R.w/2, R.y + R.h/2); + g.setColor(g.theme.fg); + g.drawString(time_string_new_wrapped, R.x + R.w/2, R.y + R.h/2); + time_string_old_wrapped = time_string_new_wrapped; + }; + + g.clear(); + loadSettings(); + + // Stop updates when LCD is off, restart when on + Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } + }); + + Bangle.setUI({ + mode : 'clock', + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (animInterval) clearInterval(animInterval); + animInterval = undefined; + require('widget_utils').show(); // re-show widgets + } + }); + + Bangle.loadWidgets(); + if (settings.showWidgets) { + Bangle.drawWidgets(); + } else { + require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe } -}); -Bangle.loadWidgets(); -if (settings.showWidgets) { - Bangle.drawWidgets(); -} else { - require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe + let R = Bangle.appRect; + draw(); } - -R = Bangle.appRect; -draw(); -} \ No newline at end of file diff --git a/apps/fuzzyw/fuzzyw.settings.js b/apps/fuzzyw/fuzzyw.settings.js index 535f91d67..2cb693a84 100644 --- a/apps/fuzzyw/fuzzyw.settings.js +++ b/apps/fuzzyw/fuzzyw.settings.js @@ -1,32 +1,39 @@ (function(back) { - const SETTINGS_FILE = "fuzzy.settings.json"; + const SETTINGS_FILE = "fuzzyw.settings.json"; // initialize with default settings... - let s = {'showWidgets': false} + let s = {'showWidgets': false, 'animate': true}; // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings - const storage = require('Storage') + const storage = require('Storage'); let settings = storage.readJSON(SETTINGS_FILE, 1) || s; - const saved = settings || {} + const saved = settings || {}; for (const key in saved) { - s[key] = saved[key] + s[key] = saved[key]; } function save() { - settings = s - storage.write(SETTINGS_FILE, settings) + settings = s; + storage.write(SETTINGS_FILE, settings); } E.showMenu({ '': { 'title': 'Fuzzy Word Clock' }, '< Back': back, 'Show Widgets': { - value: settings.showWidgets, + value: s.showWidgets, onchange: () => { - settings.showWidgets = !settings.showWidgets; + s.showWidgets = !s.showWidgets; save(); } }, + 'Animate': { + value: s.animate, + onchange: () => { + s.animate = !s.animate; + save(); + } + } }); }) diff --git a/apps/fuzzyw/metadata.json b/apps/fuzzyw/metadata.json index 97f060866..110526712 100644 --- a/apps/fuzzyw/metadata.json +++ b/apps/fuzzyw/metadata.json @@ -2,7 +2,7 @@ "id":"fuzzyw", "name":"Fuzzy Text Clock", "shortName": "Fuzzy Text", - "version": "0.04", + "version": "0.05", "description": "An imprecise clock for when you're not in a rush", "readme": "README.md", "icon":"fuzzyw.png", diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index b2e5b37fe..6c47cf3f2 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -12,7 +12,7 @@ see the Bangle.js 1 instructions

    -

    Your current firmware version is unknown and DFU is unknown. +

    Your current firmware version is unknown and DFU is unknown. The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.

`; - }); - if (trackList.length==0) { + }); + if (trackList.length==0) { html += `
@@ -245,9 +271,15 @@ ${trackData.Latitude ? `
`; - } - html += ` + } + html += ` +

Batch

+
+ + + +

Settings

`; - domTracks.innerHTML = html; - document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{ - var allowNoGPS = event.target.checked; - localStorage.setItem("recorder-allow-no-gps", allowNoGPS); - }); - Util.hideModal(); - var buttons = domTracks.querySelectorAll("button"); + domTracks.innerHTML = html; + document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{ + var allowNoGPS = event.target.checked; + localStorage.setItem("recorder-allow-no-gps", allowNoGPS); + }); + Util.hideModal(); + var buttons = domTracks.querySelectorAll("button"); for (var i=0;i { - var button = event.currentTarget; - var filename = button.getAttribute("filename"); - var trackid = parseInt(button.getAttribute("trackid")); - if (!filename || trackid===undefined) return; - var task = button.getAttribute("task"); - if (task=="delete") { - Util.showModal("Deleting Track..."); - Util.eraseStorageFile(filename,()=>{ - Util.hideModal(); - getTrackList(); - }); - } - if (task=="downloadkml") { - downloadTrack(filename, track => saveKML(track, `Bangle.js Track ${trackid}`)); - } - if (task=="downloadgpx") { - downloadTrack(filename, track => saveGPX(track, `Bangle.js Track ${trackid}`)); - } - if (task=="downloadcsv") { - downloadTrack(filename, track => saveCSV(track, `Bangle.js Track ${trackid}`)); - } - }); - } - }); + buttons[i].addEventListener("click",event => { + var button = event.currentTarget; + var filename = button.getAttribute("filename"); + var trackid = parseInt(button.getAttribute("trackid")); + var task = button.getAttribute("task"); + + if (!/_all$/.test(task) && (!filename || trackid===undefined)) return; + + switch(task) { + case "delete": + Util.showModal(`Deleting ${filename}...`); + Util.eraseStorageFile(filename,()=>{ + Util.hideModal(); + getTrackList(); + }); + break; + + case "downloadkml": + downloadTrack(filename, track => saveKML(track, `Bangle.js Track ${trackid}`)); + break; + case "downloadgpx": + downloadTrack(filename, track => saveGPX(track, `Bangle.js Track ${trackid}`)); + break; + case "downloadcsv": + downloadTrack(filename, track => saveCSV(track, `Bangle.js Track ${trackid}`)); + break; + + case "downloadkml_all": + downloadAll(trackList, saveKML); + break; + case "downloadgpx_all": + downloadAll(trackList, saveGPX); + break; + case "downloadcsv_all": + downloadAll(trackList, saveCSV); + break; + } + }); + } + }); }); } diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index a231a98e9..364bb7bf6 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.38", + "version": "0.43", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget,clkinfo", diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 057130ff0..74cffb068 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -1,8 +1,8 @@ { let storageFile; // file for GPS track - let entriesWritten = 0; let activeRecorders = []; - let writeSetup; + let writeSetup; // the interval for writing, or 'true' if using GPS + let writeSubSecs; // true if we should write .1s for time, otherwise round to nearest second let loadSettings = function() { var settings = require("Storage").readJSON("recorder.json",1)||{}; @@ -63,7 +63,7 @@ function onHRM(h) { bpmConfidence = h.confidence; bpm = h.bpm; - srv = h.src; + src = h.src; } return { name : "HR", @@ -176,11 +176,10 @@ }; let getCSVHeaders = activeRecorders => ["Time"].concat(activeRecorders.map(r=>r.fields)); - let writeLog = function(period) { - entriesWritten++; + let writeLog = function() { WIDGETS["recorder"].draw(); try { - var fields = [period===1?getTime().toFixed(1):Math.round(getTime())]; + var fields = [writeSubSecs?getTime().toFixed(1):Math.round(getTime())]; activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.getValues())); if (storageFile) storageFile.write(fields.join(",")+"\n"); } catch(e) { @@ -194,18 +193,15 @@ } } - let writeOnGPS = function() {writeLog(settings.period);}; - // Called by the GPS app to reload settings and decide what to do let reload = function() { var settings = loadSettings(); if (typeof writeSetup === "number") clearInterval(writeSetup); writeSetup = undefined; - Bangle.removeListener('GPS', writeOnGPS); + Bangle.removeListener('GPS', writeLog); activeRecorders.forEach(rec => rec.stop()); activeRecorders = []; - entriesWritten = 0; if (settings.recording) { // set up recorders @@ -225,8 +221,9 @@ } // start recording... WIDGETS["recorder"].draw(); + writeSubSecs = settings.period===1; if (settings.period===1 && settings.record.includes("gps")) { - Bangle.on('GPS', writeOnGPS); + Bangle.on('GPS', writeLog); writeSetup = true; } else { writeSetup = setInterval(writeLog, settings.period*1000, settings.period); diff --git a/apps/red7game/ChangeLog b/apps/red7game/ChangeLog index ffe131874..fffef63d5 100644 --- a/apps/red7game/ChangeLog +++ b/apps/red7game/ChangeLog @@ -4,3 +4,4 @@ 0.04: Update cards to draw rounded on newer firmware. Make sure in-game menu can't be pulled up during end of game. 0.05: add confirmation prompt to new game to prevent fat fingering new game during existing one. 0.06: fix AI logic typo and add prompt to show what AI played each turn. +0.07: Minor code improvements. diff --git a/apps/red7game/metadata.json b/apps/red7game/metadata.json index eeb1cfcb8..5f41740ed 100644 --- a/apps/red7game/metadata.json +++ b/apps/red7game/metadata.json @@ -2,7 +2,7 @@ "name": "Red 7 Card Game", "shortName" : "Red 7", "icon": "icon.png", - "version":"0.06", + "version":"0.07", "description": "An implementation of the card game Red 7 for your watch. Play against the AI and be the last player still in the game to win!", "tags": "game", "supports":["BANGLEJS2"], diff --git a/apps/red7game/red7.js b/apps/red7game/red7.js index 697d36f97..f98a525ed 100644 --- a/apps/red7game/red7.js +++ b/apps/red7game/red7.js @@ -486,8 +486,7 @@ function canPlay(hand, palette, otherPalette) { } else { //Check if any palette play can win with rule. for(let h of hand.handCards) { - if(h === c) {} - else { + if(h !== c) { clonePalette.addCard(c); if(isWinningCombo(c, clonePalette, otherPalette)) { return true; @@ -531,8 +530,7 @@ class AI { } else { //Check if any palette play can win with rule. for(let h of this.hand.handCards) { - if(h === c) {} - else { + if(h !== c) { clonePalette.addCard(h); if(isWinningCombo(c, clonePalette, otherPalette)) { ruleStack.addCard(c); @@ -556,7 +554,7 @@ function shuffleDeck(deckArray) { } var deck = []; -var screen = 1; +//var screen = 1; var startedGame = false; var playerHand = new Hand(); var playerPalette = new Hand(); @@ -621,10 +619,10 @@ function drawScreen1() { Bangle.on('swipe', function(direction){ if(direction === -1) { drawScreen2(); - screen = 2; + //screen = 2; } else if(direction === 1) { drawScreen1(); - screen = 1; + //screen = 1; } }); g.setBgColor(0,0,0); diff --git a/apps/regattatimer/ChangeLog b/apps/regattatimer/ChangeLog new file mode 100644 index 000000000..9f342060d --- /dev/null +++ b/apps/regattatimer/ChangeLog @@ -0,0 +1,7 @@ +0.10: (2024-02-23) initial alpha upload +0.20: (2024-02-23) fixed minor issues with settings +0.30: (2024-03-01) advanced settings, rearanged ui elements, fixed rendering problems +0.31: (2024-03-06) fixed typos in readme, removed some unnecessary code +0.32: replaced icons with "Graphics.createImage" +0.40: Fixing lint warnings for unused vars +0.41: removed no longer required import of "heatshrink" diff --git a/apps/regattatimer/README.md b/apps/regattatimer/README.md new file mode 100644 index 000000000..f93086fe5 --- /dev/null +++ b/apps/regattatimer/README.md @@ -0,0 +1,71 @@ +# Regatta Timer 5-4-1 countdown + +## Modes + +* **Idle** + On startup the application is in idle mode showing a large 5 in the centre of the screen and the time of day below. + `Button` switches to start mode. +* **Start** + During the countdown, the screen changes the layout several times to use as much space as + possible to display the numbers. + When time is up the buzzer sounds and the application switches to race mode. + `Button` switches to idle mode. +* **Race** + Race time, local time, SOA, number reachable GPS satellites and battery level are shown. + `Button` switches to "stopped mode". +* **Stoped** + The race counter stops. + `Button` switches to idle mode. + +## Screenshots + +*Idle mode: showing a big 5 and time of day below* + +![Idle mode: showing a big 5 and time of day below](screenshot-1.png) + +*Start mode: minutes and seconds* + +![Start mode: minutes and seconds](screenshot-2.png) + +*Start mode: seconds* + +![Start mode: seconds](screenshot-3.png) + +*Race mode: elapsed time, time of day, speed, satellites, battery* + +![Race mode: elapsed time, time of day, speed, satellites, battery](screenshot-4.png) + +*Race mode: with german abbreviations* + +![Race mode: with german abbreviations](screenshot-5.png) + +*Settings page: main* + +![Settings page: main](screenshot-6.png) + +*Settings page: choose the theme* + +![Settings page: choose the theme](screenshot-7.png) + +## Localization + +Localization is done by the Bangle.js 2 app "Languages" +* Go to [banglejs.com/apps](https://banglejs.com/apps/) +* Search for app "Languages" +* Click the "arrow up" or "burger" icon +* Choose your language from the dropdown +* Click `upload` + +**Some nautical abbreviations which are not part of the "Languages" app are stored in `translations.json`.** + +## Feedback + +Report bugs or request a feature at [github.com/naden](https://github.com/naden) + +## Roadmap +* add a second coundown layout; mimic a classic regatta chronograph +* add recording of gps course and race time +* add flag icons for start mode screen + +## Created by +© 2021 - 2024 [naden.de](https://naden.de) diff --git a/apps/regattatimer/app-icon.js b/apps/regattatimer/app-icon.js new file mode 100644 index 000000000..71417f0cd --- /dev/null +++ b/apps/regattatimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/8H/A4NgtfgzkgoVg50g40Awg5lnmSpMk4ARMkMkCQNgCJkJCKM////0AQLgNJkBZP5OACB0B5OKjJqNgGJLINOEZprDCJ0rmVJoARMzfAg1JLh8AQCIRNGQPghg1NhQgBghZOhMmoR9ClmSqDXJR4NLAwMhknQEhEsCINwboQRKgG59RuDCJYADLgIRPqVnCJ9CuYROgOSwYjPyUDCJ0IzwRP4mQCIsRlARH8mZWYPJbgK/BCJOSR4OTCIMGCJ8MAoIRIi3btUk3UACJYABLIcapMJCJxZLCKbMFCP4R/COQAo")) diff --git a/apps/regattatimer/app.js b/apps/regattatimer/app.js new file mode 100644 index 000000000..e1ff64b6a --- /dev/null +++ b/apps/regattatimer/app.js @@ -0,0 +1,348 @@ +/** + * Regatta Timer + */ +const Layout = require("Layout"); +const locale = require("locale").name == "system" ? "en" : require("locale").name.substring(0, 2); + +// "Anton" bold font +Graphics.prototype.setFontAnton = function(scale) { + // Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); +}; + +function Regattatimer() { + return { + layout: undefined, + /* + layouts: { + idle: function() { + switch(settings.dial) { + case "Discs": + break; + case "Numeric": + default: + break; + } + }, + start: function(phase) { + switch(settings.dial) { + case "Discs": + break; + case "Numeric": + default: + break; + } + }, + race: function() { + } + }, + */ + mode: "idle", // idle, start, race" + countdown: 300, // 5 minutes + counter: undefined, + interval: undefined, + theme: null, + themes: { + "Light": { + "fgColor": "#000000", + "bgColor": "#FFFF00", + }, + "Dark": { + "fgColor": "#FFFF00", + "bgColor": "#000000", + } + }, + icons: { + "battery": function() { + return Graphics.createImage(` + XXXX +X X +X XX X +X X +X XX X +X X +X XX X +X X +X XX X +X X +XXXXXX`); + }, + "satellites": function() { + return Graphics.createImage(` + X + XoX + XoX + XoX + XoX o X + XoX o o XoX +XoX o o XoX + X o XoX + XoX + XoX + XoX + X`); + }, + }, + settings: Object.assign({ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark", + }, require('Storage').readJSON("regattatimer.json", true) || {}), + + translations: Object.assign({ + "de": { + "speed": "FüG", // Fahrt über Grund + "speed_unit": "kn" + }, + "en": { + "speed": "SOA", // SOA speed of advance + "speed_unit": "kn" + } + }, require('Storage').readJSON("translations.json", true) || {}), + + init: function() { + + if(this.settings.debug) { + this.countdown = 1; + } + + this.theme = this.themes[this.settings.theme]; + + Bangle.setLCDPower(1); + Bangle.setLCDTimeout(0); + + // in "idle", "start" or "stoped" mode, a button click (re)starts the countdown + // in "race" mode, a button click stops the counter + var onButtonClick = (function(ev) { + switch(this.mode) { + case "idle": + this.resetCounter(); + this.mode = "start"; + this.setLayoutStartMinSec(); + this.startCounter(); + this.interval = setInterval((function() { + this.startCounter(); + }).bind(this), 1000); + break; + case "stoped": + case "start": + this.resetCounter(); + this.setLayoutIdle(); + break; + case "race": + this.raceCounterStop(); + break; + } + }).bind(this); + + setWatch(onButtonClick, BTN1, true); + + this.setLayoutIdle(); + }, + + onGPS: function(fix) { + if(this.mode == "race") { + if(fix.fix && isFinite(fix.speed)) { + this.layout.clear(layout.speed); + this.layout.speed.label = fix.speed.toFixed(2); + this.layout.render(this.layout.speed); + } + this.layout.satellites.label = fix.satellites; + } + }, + + translate: function(slug) { + return this.translations[locale][slug]; + }, + // during the start phase, the clock counts down 5 4 1 0 minutes + // a button click restarts the countdown + startCounter: function() { + + this.counter --; + + if(this.counter >= 0) { + var counterMinutes = parseInt(this.counter / 60); + + if(counterMinutes > 0) { + this.layout.minutes.label = counterMinutes; + // this.layout.seconds.label = "0".concat(this.counter - counterMinutes * 60).toString().slice(-2); + this.layout.seconds.label = this.padZeroLeft(this.counter - counterMinutes * 60); + this.layout.render(); + } + else { + this.setLayoutStartSec(); + this.layout.seconds.label = this.counter.toString(); + this.layout.render(); + } + // this keeps the watch LCD lit up + g.flip(); + } + // time is up + else { + this.raceCounterStart(); + } + }, + padZeroLeft: function(str) { + return str.toString().padStart(2, "0"); + }, + formatTime: function(time) { + var + minutes = parseInt(time / 60), + seconds = time - (minutes * 60); + + return this.padZeroLeft(parseInt(time / 3600)) + ":" + this.padZeroLeft(minutes) + ":" + this.padZeroLeft(seconds); + }, + raceCounter: function() { + + if(this.counter % 60 == 0) { + this.layout.clear(this.layout.battery); + this.layout.battery.label = E.getBattery() + "%"; + this.layout.render(this.layout.battery); + } + + this.counter ++; + + this.layout.racetime.label = this.formatTime(this.counter); + this.layout.daytime.label = require("locale").time(new Date(), 1); + this.layout.render(); + + // keeps the watch screen lit up + g.flip(); + }, + raceCounterStop: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + this.mode = "stoped"; + }, + raceCounterStart: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + + if(this.settings.buzzer) { + Bangle.buzz(); + } + + this.counter = 0; + // switch to race mode + this.mode = "race"; + this.setLayoutRace(); + this.raceCounter(); + this.interval = setInterval((function() { + this.raceCounter(); + }).bind(this), 1000); + }, + + resetCounter: function() { + if(this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + this.counter = this.countdown; + }, + + setLayoutIdle: function() { + + g.clear(); + + this.mode = "idle"; + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + { + type: "v", + c: [ + {type: "txt", font: "Anton", label: "5", col: this.theme.fgColor, id: "minutes", fillx: 1, filly: 1}, + {type: "txt", font: "20%", label: "--:--", col: this.theme.fgColor, id: "daytime", fillx: 1, filly: 1} + ] + } + ]}, {lazy: true}); + + this.interval = setInterval((function() { + this.layout.daytime.label = require("locale").time(new Date(), 1); + this.layout.render(); + + // keeps the watch screen lit up + g.flip(); + }).bind(this), 1000); + }, + setLayoutStartMinSec: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + { + type: "h", + c: [ + {type: "txt", font: "Anton", label: "4", col: this.theme.fgColor, id: "minutes", fillx: 1, filly: 1}, + {type: "txt", font: "Anton", label: "59", col: this.theme.fgColor, id: "seconds", fillx: 1, filly: 1}, + ] + } + ]}, {lazy: true} + ); + }, + setLayoutStartSec: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c:[ + {type: "txt", font: "Anton", label: "", fillx: true, filly: true, col: this.theme.fgColor, id: "seconds"}, + ]}, {lazy: true}); + }, + setLayoutRace: function() { + g.clear(); + + this.layout = new Layout({ + type: "v", + bgCol: this.theme.bgColor, + c: [ + {type: "txt", font: "20%", label: "00:00:00", col: this.theme.fgColor, pad: 4, filly: 1, fillx: 1, id: "racetime"}, + {type: "txt", font: "15%", label: "-", col: this.theme.fgColor, pad: 4, filly:1, fillx:1, id: "daytime"}, + // horizontal + {type: "h", c: [ + {type: "txt", font: "10%", label: this.translate("speed"), col: this.theme.fgColor, pad:4, fillx:1, filly:1}, + {type: "txt", font: "20%", label: "0", col: this.theme.fgColor, pad:4, fillx:1, filly:1, id: "speed"}, + {type: "txt", font: "10%", label: this.translate("speed_unit"), col: this.theme.fgColor, pad:4, fillx:1, filly:1}, + ]}, + {type: "h", c: [ + {type:"img", pad: 2, col: this.theme.fgColor, bgCol: this.theme.bgColor, src: this.icons.satellites()}, + {type: "txt", font: "10%", label: "0", col: this.theme.fgColor, pad: 2, filly:1, id: "satellites"}, + // hacky, use empty element with fillx to push the other elments to the left an right side + {type: undefined, pad: 2, fillx: 1}, + {type:"img", pad: 2, col: this.theme.fgColor, bgCol: this.theme.bgColor, src: this.icons.battery()}, + {type: "txt", font: "10%", label: "-", col: this.theme.fgColor, pad: 2, filly: 1, id: "battery"}, + ]} + ]}, {lazy: true}); + } + }; +} + +var regattatimer = Regattatimer(); +regattatimer.init(); + +if(regattatimer.settings.gps) { + Bangle.setGPSPower(1); + Bangle.on('GPS', regattatimer.onGPS.bind(regattatimer)); +} + +Bangle.on('kill', function() { + Bangle.setLCDPower(0); + Bangle.setLCDTimeout(10); + /* + if(regattatimer.settings.gps) { + Bangle.setGPSPower(0); + } + */ +}); diff --git a/apps/regattatimer/icon.png b/apps/regattatimer/icon.png new file mode 100644 index 000000000..5ae5f1381 Binary files /dev/null and b/apps/regattatimer/icon.png differ diff --git a/apps/regattatimer/metadata.json b/apps/regattatimer/metadata.json new file mode 100644 index 000000000..3249bb775 --- /dev/null +++ b/apps/regattatimer/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "regattatimer", + "name": "Regatta Timer", + "shortName": "RegattaTimer", + "version": "0.41", + "description": "Regatta Timer with 5-4-1 Countdown", + "icon": "icon.png", + "tags": "tool,outdoors,sailing,race,regatta,boat,timer", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "regattatimer.app.js", "url": "app.js"}, + {"name": "regattatimer.settings.js", "url": "settings.js"}, + {"name": "regattatimer.img", "url": "app-icon.js", "evaluate": true} + ], + "data": [{"name": "regattatimer.json"}], + "screenshots": [{"url": "screenshot.png"},{"url": "screenshot-1.png"},{"url": "screenshot-2.png"},{"url": "screenshot-3.png"},{"url": "screenshot-4.png"},{"url": "screenshot-5.png"},{"url": "screenshot-6.png"},{"url": "screenshot-7.png"}] +} diff --git a/apps/regattatimer/regattatimer.json b/apps/regattatimer/regattatimer.json new file mode 100644 index 000000000..e2ea78f76 --- /dev/null +++ b/apps/regattatimer/regattatimer.json @@ -0,0 +1,8 @@ +{ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark" +} diff --git a/apps/regattatimer/regattatimer.translations.json b/apps/regattatimer/regattatimer.translations.json new file mode 100644 index 000000000..e89eeddf8 --- /dev/null +++ b/apps/regattatimer/regattatimer.translations.json @@ -0,0 +1,10 @@ +{ + "de": { + "speed": "FüG", + "speed_unit": "kn" + }, + "en": { + "speed": "SOA", + "speed_unit": "kn" + } +} diff --git a/apps/regattatimer/screenshot-1.png b/apps/regattatimer/screenshot-1.png new file mode 100644 index 000000000..6888ccd62 Binary files /dev/null and b/apps/regattatimer/screenshot-1.png differ diff --git a/apps/regattatimer/screenshot-2.png b/apps/regattatimer/screenshot-2.png new file mode 100644 index 000000000..bde59766d Binary files /dev/null and b/apps/regattatimer/screenshot-2.png differ diff --git a/apps/regattatimer/screenshot-3.png b/apps/regattatimer/screenshot-3.png new file mode 100644 index 000000000..4416a0fc8 Binary files /dev/null and b/apps/regattatimer/screenshot-3.png differ diff --git a/apps/regattatimer/screenshot-4.png b/apps/regattatimer/screenshot-4.png new file mode 100644 index 000000000..23cb79ac7 Binary files /dev/null and b/apps/regattatimer/screenshot-4.png differ diff --git a/apps/regattatimer/screenshot-5.png b/apps/regattatimer/screenshot-5.png new file mode 100644 index 000000000..49613d8c0 Binary files /dev/null and b/apps/regattatimer/screenshot-5.png differ diff --git a/apps/regattatimer/screenshot-6.png b/apps/regattatimer/screenshot-6.png new file mode 100644 index 000000000..391eaafd2 Binary files /dev/null and b/apps/regattatimer/screenshot-6.png differ diff --git a/apps/regattatimer/screenshot-7.png b/apps/regattatimer/screenshot-7.png new file mode 100644 index 000000000..e1f96005f Binary files /dev/null and b/apps/regattatimer/screenshot-7.png differ diff --git a/apps/regattatimer/screenshot.png b/apps/regattatimer/screenshot.png new file mode 100644 index 000000000..23cb79ac7 Binary files /dev/null and b/apps/regattatimer/screenshot.png differ diff --git a/apps/regattatimer/settings.js b/apps/regattatimer/settings.js new file mode 100644 index 000000000..7e43322ba --- /dev/null +++ b/apps/regattatimer/settings.js @@ -0,0 +1,74 @@ +(function(back) { + var + file = "regattatimer.json", + storage = require("Storage"), + /*dials = ["Numeric", "Discs"],*/ + themes = ["Light", "Dark"], + settings = Object.assign({ + "debug": false, + "buzzer": true, + "dial": "Numeric", + "gps": true, + "record": false, + "theme": "Dark", + }, storage.readJSON(file, true) || {}); + + function save(key, value) { + settings[key] = value; + storage.writeJSON(file, settings); + } + + E.showMenu({ + "" : { "title" : "Regatta Timer" }, + "< Back" : () => back(), + "GPS": { + value: !!settings.gps, // !! converts undefined to false + onchange: v => { + save("gps", v); + } + }, + "THEME": { + value: themes.indexOf(settings.theme), + min: 0, + max: themes.length - 1, + step: 1, + wrap: true, + format: v => themes[v], + onchange: (d) => { + save("theme", themes[d]); + } + }, + "BUZZER": { + value: !!settings.buzzer, // !! converts undefined to false + onchange: v => { + save("buzzer", v); + } + }, + /* + "DIAL": { + value: dials.indexOf(settings.dial), + min: 0, + max: dials.length - 1, + step: 1, + wrap: true, + format: v => dials[v], + onchange: (d) => { + save("dial", dials[d]); + } + }, + "RECORD": { + value: !!settings.record, // 0| converts undefined to 0 + onchange: v => { + settings.record = v; + save("record", v); + } + }, + */ + "DEBUG": { + value: !!settings.debug, // 0| converts undefined to 0 + onchange: v => { + save("debug", v); + } + }, + }); +}) diff --git a/apps/rellotge/ChangeLog b/apps/rellotge/ChangeLog new file mode 100644 index 000000000..fc907fa45 --- /dev/null +++ b/apps/rellotge/ChangeLog @@ -0,0 +1,10 @@ +0.10: Llançament inicial +0.20: Esmenat problema amb les hores passada l'hora en punt (apareixia l'hora anterior) +0.30: Redisseny de l'aplicatiu, text alineat a l'esquerra, font canviada, treiem dades innecessàries +0.40: Afegits suggeriments de l'usuari @bobrippling al codi +0.50: Fixing lint warnings for unused vars +0.60: Fixes typos, BTN1 to show launcher and show app icon +0.61: Minor code improvements +0.70: Better wrapping of the text base (dynamic instead of hardcoded) +0.80: Add analog watch, steps and date +0.85: remapping of the minute intervals, to add accuracy. \ No newline at end of file diff --git a/apps/rellotge/README.md b/apps/rellotge/README.md new file mode 100644 index 000000000..b01943444 --- /dev/null +++ b/apps/rellotge/README.md @@ -0,0 +1,13 @@ +# Rellotge, una aplicació per llegir l'hora en català + +### Info in english + +This is a simple app, used to show you the time using the traditiona catalan way, which follows the system used by bell towers that usually ring the bells at every quarter of hour. So, you have quarters, half quarters and saying like "tocats" (just rung) or "ben tocats" (well rung). + +### Informació en català + +Ara ja pots llegir l'hora en català com un professional amb aquesta aplicació. Domina els quarts, els mitjos quarts, tocats i ben tocats en cada moment del dia! + +### Informacio en esperanto + +Tiu ĉi apo permesas vin indiki kioma horo estas laŭ la kataluna tradicia sistemo. diff --git a/apps/rellotge/icona.js b/apps/rellotge/icona.js new file mode 100644 index 000000000..91a5b1460 --- /dev/null +++ b/apps/rellotge/icona.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCpMkyQC/AX4CepASPiVIxIRQyIRQyQRRo3btu2ARRZBAIIRQpMbCKEiI54GBCKKSMCIdkyYRP8mSuxZNp4RBpwRMy////ypObEZl//o4BmRHMv/8CKE9AwJrNrMlLJwYDPpwRBybpPkmZa6HJCJ8mfaCMKCItIKxRZFowRQkwRPyJELI4mJCKAOMCIYQPAX4C/ASIA=")) diff --git a/apps/rellotge/icona.png b/apps/rellotge/icona.png new file mode 100644 index 000000000..258cdecab Binary files /dev/null and b/apps/rellotge/icona.png differ diff --git a/apps/rellotge/metadata.json b/apps/rellotge/metadata.json new file mode 100644 index 000000000..955fb4d87 --- /dev/null +++ b/apps/rellotge/metadata.json @@ -0,0 +1,15 @@ +{ "id": "rellotge", + "name": "Rellotge en catala", + "shortName":"Rellotge", + "version": "0.85", + "description": "A clock with traditional naming of hours in Catalan", + "icon": "icona.png", + "readme": "README.md", + "type": "clock", + "tags": "", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"rellotge.app.js","url":"rellotge.js"}, + {"name":"rellotge.img","url":"icona.js","evaluate":true} + ] +} diff --git a/apps/rellotge/rellotge.js b/apps/rellotge/rellotge.js new file mode 100644 index 000000000..2e36059ef --- /dev/null +++ b/apps/rellotge/rellotge.js @@ -0,0 +1,240 @@ +// Example application code +// Taken from https://github.com/espruino/BangleApps/blob/master/apps/sclock/clock-simple.js +(function() { + + const timeFontSize = 1; + const dateFontSize = 2; + const font = "12x20"; + + const Panel = { + STEPS: 0, + DATE: 1 + }; + + let panel = Panel.STEPS; + + const timeTextMagin = 15; + const xyCenter = timeTextMagin; + const yposTime = 45; + const leshores = ["Les dotze","La una","Les dues","Les tres","Les quatre","Les cinc","Les sis","Les set","Les vuit","Les nou","Les deu","Les onze","Les dotze","La una","Les dues","Les tres","Les quatre","Les cinc","Les sis","Les set","Les vuit","Les nou","Les deu","Les onze","Les dotze"]; + const leshores2 = ["d'una","de dues","de tres","de quatre","de cinc","de sis","de set","de vuit","de nou","de deu","d'onze","de dotze"]; + const RED = '#f00'; + const BLACK = "#000" + + function getHora(hour) { + if (hour >= 12) { + hour -= 12; + } + return leshores2[hour]; + } + + function addLineFeeds(inputString, g, posX) { + const margin = timeTextMagin; + const words = inputString.split(' '); + let lines = ""; + let line = ""; + const totalWidth = g.getWidth(); + + for (const word of words) { + const nextLine = line + word; + const width = posX + g.stringWidth(nextLine) + margin; + + if (width > totalWidth) { + lines += line.trim() + "\r\n"; + line = ""; + } + line += word + " "; + } + lines += line.trim(); + return lines; + } + + // Define the center coordinates of the watch face + const margin = 10; + const centerX = 40 + margin; + const centerY = g.getHeight() - 40 - margin; + + // Function to draw the watch face + function drawWatchFace() { + + const diameter = 40; + g.setColor(BLACK); + g.drawCircle(centerX, centerY, diameter); + + // Draw hour markers + for (let i = 0; i < 12; i++) { + const angle = (i / 12) * Math.PI * 2; + const x1 = centerX + Math.sin(angle) * 70 / 2; + const y1 = centerY - Math.cos(angle) * 70 / 2; + const x2 = centerX + Math.sin(angle) * 60 / 2; + const y2 = centerY - Math.cos(angle) * 60 / 2; + g.drawLine(x1, y1, x2, y2); + } + } + + function drawHand(centerX, centerY, hourAngle, handLength) { + const hourHandX = centerX + Math.sin(hourAngle) * handLength; + const hourHandY = centerY - Math.cos(hourAngle) * handLength; + g.drawLine(centerX, centerY, hourHandX, hourHandY); + } + + // Function to update the watch display + function updateWatch() { + const now = new Date(); + const hours = now.getHours() % 12; + const minutes = now.getMinutes(); + + // Calculate angles for hour, minute, and second hands + const hourAngle = ((hours + minutes / 60) / 12) * Math.PI * 2; + const minuteAngle = (minutes / 60) * Math.PI * 2; + g.setColor(BLACK); + + drawHand(centerX, centerY, hourAngle, 10); + drawHand(centerX, centerY, minuteAngle, 15); + } + + function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; + } + + function drawDate() { + g.setFont(font, dateFontSize); + + const date = new Date(); + const dow = require("locale").dow(date, 2).toUpperCase(); //dj. + g.drawString(dow, g.getWidth() - 60, g.getHeight() - 60, true); + + const mon = date.getDate() + " " + require("locale").month(date, 1); + g.setFont(font, "4x6"); + g.drawString(mon, g.getWidth() - 70, g.getHeight() - 25, true); + } + + function drawSteps() { + + g.setFont(font, dateFontSize); + const steps = getSteps() + g.drawString(steps, g.getWidth() - 60, g.getHeight() - 60, true); + + g.setFont(font, "4x6"); + const text = "Passos" + g.drawString(text, g.getWidth() - 70, g.getHeight() - 25, true); + } + + function drawSimpleClock() { + + // get date + var d = new Date(); + var m = d.getMinutes(); + + let t; + if (m == 0) { + t = leshores[d.getHours()] + " en punt"; + } else if (m >= 1 && m < 4) { + t = leshores[d.getHours()] + " tocades"; + } else if (m >= 4 && m < 7) { + t = leshores[d.getHours()] + " ben tocades"; + } else if (m == 7) { + t = "Mig quart " + getHora(d.getHours()); + } else if (m >= 8 && m < 12) { + t = "Mig quart tocat " + getHora(d.getHours()); + } else if (m >= 12 && m < 15) { + t = "Mig quart ben tocat " + getHora(d.getHours()); + } else if (m == 15) { + t = "Un quart " + getHora(d.getHours()); + } else if (m >= 16 && m < 19) { + t = "Un quart tocat " + getHora(d.getHours()); + } else if (m >= 19 && m < 22) { + t = "Un quart ben tocat " + getHora(d.getHours()); + } else if (m == 22) { + t = "Un quart i mig " + getHora(d.getHours()); + } else if (m >= 23 && m < 26) { + t = "Un quart i mig tocat " + getHora(d.getHours()); + } else if (m >= 26 && m < 30) { + t = "Un quart i mig ben tocat " + getHora(d.getHours()); + } else if (m == 30) { + t = "Dos quarts " + getHora(d.getHours()); + } else if (m >= 31 && m < 34) { + t = "Dos quarts tocats " + getHora(d.getHours()); + } else if (m >= 34 && m < 37) { + t = "Dos quarts ben tocats " + getHora(d.getHours()); + } else if (m == 37) { + t = "Dos quarts i mig " + getHora(d.getHours()); + } else if (m >= 38 && m < 42) { + t = "Dos quarts i mig tocats " + getHora(d.getHours()); + } else if (m >= 42 && m < 45) { + t = "Dos quarts i mig ben tocats " + getHora(d.getHours()); + } else if (m == 45) { + t = "Tres quarts " + getHora(d.getHours()); + } else if (m >= 46 && m < 49) { + t = "Tres quarts tocats " + getHora(d.getHours()); + } else if (m >= 49 && m < 52) { + t = "Tres quarts ben tocats " + getHora(d.getHours()); + } else if (m == 52) { + t = "Tres quarts i mig " + getHora(d.getHours()); + } else if (m >= 53 && m < 56) { + t = "Tres quarts i mig tocats " + getHora(d.getHours()); + } else if (m >= 57) { + t = "Tres quarts i mig ben tocats " + getHora(d.getHours()); + } + g.clearRect(Bangle.appRect); + // drawString centered + g.setFontAlign(-1, 0); + + g.setFont(font, timeFontSize); + t = addLineFeeds(t, g, xyCenter); + + let color; + if (E.getBattery() < 15) { + color = RED; + } + else { + color = BLACK; + } + + g.setColor(color); + g.drawString(t, xyCenter, yposTime, true); + g.setColor(BLACK); + if (panel == Panel.STEPS) { + drawSteps(); + panel = Panel.DATE; + } else { + drawDate(); + panel = Panel.STEPS; + } + + drawWatchFace(); + updateWatch(); + } + + // handle switch display on by pressing BTN1 + function onLcd(on) { + if (on) { + Bangle.drawWidgets(); + //drawSimpleClock(); + Bangle.removeListener('lcdPower', onLcd); + } + } + Bangle.on('lcdPower', onLcd); + Bangle.setUI({ + mode: "clockupdown" + }, + btn => { + // up & down even which forces panel switch + drawSimpleClock(); + }); + + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); + + // clean app screen + g.clear(); + + // refesh every 60 sec + setInterval(drawSimpleClock, 60E3); + + // draw now + drawSimpleClock(); + +})(); diff --git a/apps/rep/app.ts b/apps/rep/app.ts index d1dd67179..14fb45482 100644 --- a/apps/rep/app.ts +++ b/apps/rep/app.ts @@ -196,7 +196,7 @@ const layout = new L({ ] } ] -}, {lazy: true}); +} as const, {lazy: true}); class State { paused: boolean = true; @@ -248,7 +248,7 @@ class State { } } -const repToLabel = (i: number, id: string) => { +const repToLabel = (i: number, id: "cur" | "next") => { const rep = reps[i]; if(rep) layout[`${id}_name`]!.label = `${rep.label} / ${msToMinSec(rep.dur)}`; @@ -256,7 +256,7 @@ const repToLabel = (i: number, id: string) => { emptyLabel(id); }; -const emptyLabel = (id: string) => { +const emptyLabel = (id: "cur" | "next") => { layout[`${id}_name`]!.label = " / 0m"; }; diff --git a/apps/rep/interface.html b/apps/rep/interface.html index e14fea4c9..5356b091f 100644 --- a/apps/rep/interface.html +++ b/apps/rep/interface.html @@ -101,7 +101,8 @@ function getData() { uploadBtn.disabled = true; Util.showModal("Loading..."); - Util.readStorageJSON(repJson, reps => { + Util.readStorageJSON(repJson, reps_ => { + reps = reps_; Util.hideModal(); for(const rep of reps){ renderRep(rep); diff --git a/apps/reply/ChangeLog b/apps/reply/ChangeLog new file mode 100644 index 000000000..f3c7b0d2c --- /dev/null +++ b/apps/reply/ChangeLog @@ -0,0 +1 @@ +0.01: New Library! \ No newline at end of file diff --git a/apps/reply/README.md b/apps/reply/README.md new file mode 100644 index 000000000..dc874d183 --- /dev/null +++ b/apps/reply/README.md @@ -0,0 +1,23 @@ +# Canned Replies Library + +A library that handles replying to messages received from Gadgetbridge/Messages apps. + +## Replying to a message +The user can define a set of canned responses via the customise page after installing the app, or alternatively if they have a keyboard installed, they can type a response back. The requesting app will receive either an object containing the full reply for GadgetBridge, or a string with the response from the user, depending on how they wish to handle the response. + +## Integrating in your app +To use this in your app, simply call + +```js +require("reply").reply(/*options*/{...}).then(result => ...); +``` + +The ```options``` object can contain the following: + +- ```msg```: A message object containing a field ```id```, the ID to respond to. If this is included in options, the result of the promise will be an object as follows: ```{t: "notify", id: msg.id, n: "REPLY", msg: "USER REPLY"}```. If not included, the result of the promise will be an object, ```{msg: "USER REPLY"}``` +- ```shouldReply```: Whether or not the library should send the response over Bluetooth with ```Bluetooth.println(...```. Useful if the calling app wants to handle the response a different way. Default is true. +- ```title```: The title to show at the top of the menu. Defaults to ```"Reply with:"```. +- ```fileOverride```: An override file to read canned responses from, which is an array of objects each with a ```text``` property. Default is ```replies.json```. Useful for apps which might want to make use of custom canned responses. + +## Known Issues +Emojis are currently not supported. \ No newline at end of file diff --git a/apps/reply/app.png b/apps/reply/app.png new file mode 100644 index 000000000..bef8338cf Binary files /dev/null and b/apps/reply/app.png differ diff --git a/apps/reply/interface.html b/apps/reply/interface.html new file mode 100644 index 000000000..ddad4ef35 --- /dev/null +++ b/apps/reply/interface.html @@ -0,0 +1,122 @@ + + + + + + + + +
+ + + +
+
+
+ +
+
+
+
+
+
+

Loading

+

Syncing custom replies with your watch

+
+
+
+
+
+ +
+

No custom replies

+

Use the field above to add a custom reply

+
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/apps/reply/lib.js b/apps/reply/lib.js new file mode 100644 index 000000000..4a040c557 --- /dev/null +++ b/apps/reply/lib.js @@ -0,0 +1,69 @@ +exports.reply = function (options) { + var keyboard = "textinput"; + try { + keyboard = require(keyboard); + } catch (e) { + keyboard = null; + } + + function constructReply(msg, replyText, resolve) { + var responseMessage = {msg: replyText}; + if (msg.id) { + responseMessage = { t: "notify", id: msg.id, n: "REPLY", msg: replyText }; + } + E.showMenu(); + if (options.sendReply == null || options.sendReply) { + Bluetooth.println(JSON.stringify(responseMessage)); + } + resolve(responseMessage); + } + + return new Promise((resolve, reject) => { + var menu = { + "": { + title: options.title || /*LANG*/ "Reply with:", + back: function () { + E.showMenu(); + reject("User pressed back"); + }, + }, // options + /*LANG*/ "Compose": function () { + keyboard.input().then((result) => { + constructReply(options.msg ?? {}, result, resolve); + }); + }, + }; + var replies = + require("Storage").readJSON( + options.fileOverride || "replies.json", + true + ) || []; + replies.forEach((reply) => { + menu = Object.defineProperty(menu, reply.text, { + value: () => constructReply(options.msg ?? {}, reply.text, resolve), + }); + }); + if (!keyboard) delete menu[/*LANG*/ "Compose"]; + + if (replies.length == 0) { + if (!keyboard) { + E.showPrompt( + /*LANG*/ "Please install a keyboard app, or set a custom reply via the app loader!", + { + buttons: { Ok: true }, + remove: function () { + reject( + "Please install a keyboard app, or set a custom reply via the app loader!" + ); + }, + } + ); + } else { + keyboard.input().then((result) => { + constructReply(options.msg.id, result, resolve); + }); + } + } + E.showMenu(menu); + }); +}; diff --git a/apps/reply/metadata.json b/apps/reply/metadata.json new file mode 100644 index 000000000..34843edd4 --- /dev/null +++ b/apps/reply/metadata.json @@ -0,0 +1,16 @@ +{ "id": "reply", + "name": "Reply Library", + "version": "0.01", + "description": "A library for replying to text messages via predefined responses or keyboard", + "icon": "app.png", + "type": "module", + "provides_modules" : ["reply"], + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"reply","url":"lib.js"} + ], + "data": [{"name":"replies.json"}] +} \ No newline at end of file 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/rings/ChangeLog b/apps/rings/ChangeLog index ecdd6b011..b41d367fa 100644 --- a/apps/rings/ChangeLog +++ b/apps/rings/ChangeLog @@ -1,2 +1,3 @@ 0.01: First rev. Could use a little optimization love. -0.02: Added battery ring, bubble background for numbers, settings and little optimaztion love. \ No newline at end of file +0.02: Added battery ring, bubble background for numbers, settings and little optimaztion love. +0.03: Minor code improvements diff --git a/apps/rings/app.js b/apps/rings/app.js index 72485a763..6fbb0839f 100644 --- a/apps/rings/app.js +++ b/apps/rings/app.js @@ -145,7 +145,7 @@ function drawMonthCircleText( text, circleSize, range, value){ grimg.transparent = 1; monthCircleTextBuffer.setColor(1,1,1); - for(z=0; z < text.length; z++){ + for(let z=0; z < text.length; z++){ tobj = { x:watch.screen.centerX, y:watch.screen.centerY, scale:1, rotate: ((z + 1) / range) * (Math.PI * 2) }; tver = [-1, 0, 1, 0, 1, -circleSize, -1, -(circleSize -21)]; tran = monthCircleTextBuffer.transformVertices(tver, tobj); diff --git a/apps/rings/metadata.json b/apps/rings/metadata.json index 706ccbcda..72addf474 100644 --- a/apps/rings/metadata.json +++ b/apps/rings/metadata.json @@ -2,7 +2,7 @@ "id": "rings", "name": "Rings - an animated watchface", "shortName": "Rings", - "version": "0.02", + "version": "0.03", "description": "Ring based watchface that animates to show the date when unlocked. Inspired by / remixed from Rinkulainen.", "icon": "app.png", "screenshots": [{ diff --git a/apps/rndmclk/ChangeLog b/apps/rndmclk/ChangeLog index 1f53ea4ae..1157a257c 100644 --- a/apps/rndmclk/ChangeLog +++ b/apps/rndmclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New widget 0.02: Less invasive, change default clock setting instead of directly loading the new clock (no longer breaks Gadgetbridge notifications) 0.03: Only changes when the widget id reloaded (no longer uses LCD turning off) +0.04: Minor code improvements diff --git a/apps/rndmclk/metadata.json b/apps/rndmclk/metadata.json index bb8e92f95..07c8fcfc1 100644 --- a/apps/rndmclk/metadata.json +++ b/apps/rndmclk/metadata.json @@ -1,7 +1,7 @@ { "id": "rndmclk", "name": "Random Clock Loader", - "version": "0.03", + "version": "0.04", "description": "Load a different clock whenever the LCD is switched on.", "icon": "rndmclk.png", "type": "widget", diff --git a/apps/rndmclk/widget.js b/apps/rndmclk/widget.js index 479d8b2c3..ceb517d74 100644 --- a/apps/rndmclk/widget.js +++ b/apps/rndmclk/widget.js @@ -19,7 +19,7 @@ // Only update the file if the clock really changed to be nice to the FLASH mem if (clockApps[clockIndex].src != currentClock) { currentClock = clockApps[clockIndex].src; - settings = require("Storage").readJSON('setting.json', 1); + const settings = require("Storage").readJSON('setting.json', 1); settings.clock = clockApps[clockIndex].src; require("Storage").write('setting.json', settings); diff --git a/apps/rolex/ChangeLog b/apps/rolex/ChangeLog index 447b084f6..8e7d73e06 100644 --- a/apps/rolex/ChangeLog +++ b/apps/rolex/ChangeLog @@ -3,3 +3,4 @@ 0.03: Made images 2 bit and fixed theme honoring 0.04: Fixed date font alignment and changed date font to match a real Rolex 0.05: Tell clock widget to hide. +0.06: Minor code improvements diff --git a/apps/rolex/app.js b/apps/rolex/app.js index fe9e534d6..adb38f451 100644 --- a/apps/rolex/app.js +++ b/apps/rolex/app.js @@ -96,7 +96,6 @@ function drawHands() { let twoPi = 2*Math.PI; let Pi = Math.PI; - let halfPi = Math.PI/2; let hourAngle = (hour+(min/60))/12 * twoPi - Pi; let minAngle = (min/60) * twoPi - Pi; diff --git a/apps/rolex/metadata.json b/apps/rolex/metadata.json index e8627246c..e5a06f55b 100644 --- a/apps/rolex/metadata.json +++ b/apps/rolex/metadata.json @@ -3,7 +3,7 @@ "shortName":"rolex", "icon": "rolex.png", "screenshots": [{"url":"screenshot1.png"}], - "version":"0.05", + "version": "0.06", "description": "A rolex like watch face", "tags": "clock", "type": "clock", diff --git a/apps/rpnsci/ChangeLog b/apps/rpnsci/ChangeLog index 35ba8b130..0ca422dbe 100644 --- a/apps/rpnsci/ChangeLog +++ b/apps/rpnsci/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app! 0.02: Bug fixes -0.03: Submitted to the app loader \ No newline at end of file +0.03: Submitted to the app loader +0.04: Minor code improvements diff --git a/apps/rpnsci/app.js b/apps/rpnsci/app.js index 5c98770c4..b38848563 100644 --- a/apps/rpnsci/app.js +++ b/apps/rpnsci/app.js @@ -303,7 +303,7 @@ let y = 0; let z = 0; let t = 0; let memJSON = storage.readJSON(MEMORY_FILE); -if (memJSON) { +if (memJSON) { // TODO: `memory` should probably be declared outside the if blocks? let memory = memJSON; } else { let memory = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; diff --git a/apps/rpnsci/metadata.json b/apps/rpnsci/metadata.json index 0c52aa8a7..6352d6258 100644 --- a/apps/rpnsci/metadata.json +++ b/apps/rpnsci/metadata.json @@ -3,7 +3,7 @@ "name": "RPN Scientific Calculator", "shortName": "Calculator", "icon": "icon.png", - "version": "0.03", + "version": "0.04", "description": "RPN scientific calculator with memory function.", "tags": "", "supports": [ diff --git a/apps/rtorch/ChangeLog b/apps/rtorch/ChangeLog index 13cbb6e72..96d5ab302 100644 --- a/apps/rtorch/ChangeLog +++ b/apps/rtorch/ChangeLog @@ -1,2 +1,3 @@ 0.01: Cloning torch and making it red :D 0.02: Modify for setUI and Bangle 2 +0.03: Minor code improvements diff --git a/apps/rtorch/app.js b/apps/rtorch/app.js index 03a50ee10..e81e0c329 100644 --- a/apps/rtorch/app.js +++ b/apps/rtorch/app.js @@ -1,7 +1,7 @@ Bangle.setLCDPower(1); Bangle.setLCDTimeout(0); g.reset(); -c = 1; +let c = 1; function setColor(delta){ c+=delta; diff --git a/apps/rtorch/metadata.json b/apps/rtorch/metadata.json index ee056ac57..5a8452e59 100644 --- a/apps/rtorch/metadata.json +++ b/apps/rtorch/metadata.json @@ -2,7 +2,7 @@ "id": "rtorch", "name": "Red Torch", "shortName": "RedTorch", - "version": "0.02", + "version": "0.03", "description": "Turns screen RED to help you see in the dark without breaking your night vision. Select from the launcher or on Bangle 1 press BTN3,BTN1,BTN3,BTN1 quickly to start when in any app that shows widgets", "icon": "app.png", "tags": "tool,torch", diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 22ab6c22f..af75a9aee 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -15,4 +15,6 @@ 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working 0.15: Keep run state between runs (allowing you to exit and restart the app) 0.16: Added ability to resume a run that was stopped previously (fix #1907) -0.17: Ensure screen redraws after "Resume run?" menu (#3044) \ No newline at end of file +0.17: Ensure screen redraws after "Resume run?" menu (#3044) +0.18: Minor code improvements +0.19: Fix step count bug when runs are resumed after a long time diff --git a/apps/run/README.md b/apps/run/README.md index 7f645b518..452549856 100644 --- a/apps/run/README.md +++ b/apps/run/README.md @@ -6,6 +6,8 @@ shows distance, time, steps, cadence, pace and more. To use it, start the app and press the middle button so that the red `STOP` in the bottom right turns to a green `RUN`. +The separate **Run+** app for Bangle.js 2 provides additional features. + ## Display * `DIST` - the distance travelled based on the GPS (if you have a GPS lock). diff --git a/apps/run/app.js b/apps/run/app.js index 0e2d78288..98b879e29 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -89,7 +89,7 @@ function onStartStop() { } } - promise = promise.then(() => { + promise.then(() => { if (running) { if (shouldResume) exs.resume() diff --git a/apps/run/metadata.json b/apps/run/metadata.json index 07fb9b85e..126d8fcf0 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,7 +1,7 @@ { "id": "run", "name": "Run", - "version":"0.17", - "description": "Displays distance, time, steps, cadence, pace and more for runners.", + "version": "0.19", + "description": "Displays distance, time, steps, cadence, pace and more for runners. The **Run+** app for Bangle.js 2 provides additional features.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps", "supports" : ["BANGLEJS","BANGLEJS2"], diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog index 96800175a..645f6cf76 100644 --- a/apps/runplus/ChangeLog +++ b/apps/runplus/ChangeLog @@ -22,4 +22,9 @@ Write to correct settings file, fixing settings not working. 0.20: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix another typo. 0.21: Rebase on "Run" app ver. 0.16. -0.22: Ensure screen redraws after "Resume run?" menu (#3044) \ No newline at end of file +0.22: Ensure screen redraws after "Resume run?" menu (#3044) +0.23: Minor code improvements +0.24: Add indicators for lock,gps and pulse to karvonen screen +0.25: Fix step count bug when runs are resumed after a long time +0.26: Add ability to zoom in on a single stat by tapping it +0.27: Allow setting to alway resume an activity diff --git a/apps/runplus/README.md b/apps/runplus/README.md index 1776e1186..d930b55e5 100644 --- a/apps/runplus/README.md +++ b/apps/runplus/README.md @@ -4,6 +4,8 @@ Displays distance, time, steps, cadence, pace and heart rate for runners. Based It requires the input of your minimum and maximum heart rate in the settings for the app to work. You can come back back to the initial run screen anytime by swimping left. To use it, start the app and press the middle button so that the red STOP in the bottom right turns to a green `RUN`. +To focus on a single stat, tap on the stat and it will take up the full screen. Tap again to return to the main screen. + ## Display 1st screen * `DIST` - the distance travelled based on the GPS (if you have a GPS lock). @@ -25,6 +27,7 @@ so if you have no GPS lock you just need to wait. Unlock the screen and navigate between displays by swiping left or right. The upper number is the limit before next heart rate zone. The lower number is the limit before previous heart rate zone. The number in the middle is the heart rate. The Z1 to Z5 number indicates the heart rate zone where you are. The circle provides a quick visualisation of the hr zone in which you are. +Indicator icons for lock, heartrate and location are updated on arrival off internal system events. The heart icon shows if the exstats module decided that the heart rate value is usable. The location icon shows if there was an GPS event with a position fix in it. ## Recording Tracks diff --git a/apps/runplus/app.js b/apps/runplus/app.js index 92428d2dc..e83112219 100644 --- a/apps/runplus/app.js +++ b/apps/runplus/app.js @@ -1,23 +1,20 @@ -// Use widget utils to show/hide widgets -let wu = require("widget_utils"); - let runInterval; -let karvonenActive = false; +let screen = "main"; // main | karvonen | menu | zoom // Run interface wrapped in a function -let ExStats = require("exstats"); +const ExStats = require("exstats"); let B2 = process.env.HWVERSION===2; let Layout = require("Layout"); let locale = require("locale"); let fontHeading = "6x8:2"; let fontValue = B2 ? "6x15:2" : "6x8:3"; +let zoomFont = "12x20:3"; +let zoomFontSmall = "12x20:2"; let headingCol = "#888"; let fixCount = 0; -let isMenuDisplayed = false; +const wu = require("widget_utils"); g.reset().clear(); Bangle.loadWidgets(); -Bangle.drawWidgets(); -wu.show(); // --------------------------- let settings = Object.assign({ @@ -29,6 +26,7 @@ let settings = Object.assign({ B5: "step", B6: "caden", paceLength: 1000, + alwaysResume: false, notify: { dist: { value: 0, @@ -56,22 +54,27 @@ function setStatus(running) { layout.button.label = running ? "STOP" : "START"; layout.status.label = running ? "RUN" : "STOP"; layout.status.bgCol = running ? "#0f0" : "#f00"; - layout.render(); + if (screen === "main") layout.render(); } // Called to start/stop running function onStartStop() { + if (screen === "karvonen") { + // start/stop on the karvonen screen reverts us to the main screen + setScreen("main"); + } + var running = !exs.state.active; - var shouldResume = false; + var shouldResume = settings.alwaysResume; var promise = Promise.resolve(); - if (running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume? + if (!shouldResume && running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume? promise = promise. then(() => { - isMenuDisplayed = true; + screen = "menu"; return E.showPrompt("Resume run?",{title:"Run"}); }).then(r => { - isMenuDisplayed=false; + screen = "main"; layout.setUI(); // grab our input handling again layout.forgetLazyState(); layout.render(); @@ -84,11 +87,11 @@ function onStartStop() { // an overwrite before we start tracking exstats if (settings.record && WIDGETS["recorder"]) { if (running) { - isMenuDisplayed = true; + screen = "menu"; promise = promise. then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })). then(() => { - isMenuDisplayed = false; + screen = "main"; layout.setUI(); // grab our input handling again layout.forgetLazyState(); layout.render(); @@ -100,10 +103,10 @@ function onStartStop() { } } - promise = promise.then(() => { + promise.then(() => { if (running) { if (shouldResume) - exs.resume() + exs.resume(); else exs.start(); } else { @@ -115,23 +118,68 @@ function onStartStop() { }); } +function zoom(statID) { + if (screen !== "main") return; + + setScreen("zoom"); + + const onTouch = () => { + Bangle.removeListener("touch", onTouch); + Bangle.removeListener("twist", onTwist); + stat.removeListener("changed", draw); + setScreen("main"); + }; + Bangle.on("touch", onTouch); // queued after layout's touchHandler (otherwise we'd be removed then instantly re-zoomed) + + const onTwist = () => { + Bangle.setLCDPower(1); + }; + Bangle.on("twist", onTwist); + + const draw = stat => { + const R = Bangle.appRect; + + g.reset() + .clearRect(R) + .setFontAlign(0, 0); + + layout.render(layout.bottom); + + const value = exs.state.active ? stat.getString() : "____"; + + g + .setFont(stat.title.length > 5 ? zoomFontSmall : zoomFont) + .setColor(headingCol) + .drawString(stat.title.toUpperCase(), R.x+R.w/2, R.y+R.h/3) + .setColor(g.theme.fg) + .drawString(value, R.x+R.w/2, R.y+R.h*2/3); + }; + layout.lazy = false; // restored when we go back to "main" + + const stat = exs.stats[statID]; + stat.on("changed", draw); + draw(stat); +} + let lc = []; // Load stats in pair by pair for (let i=0;ilayout[e.id].label = e.getString()); if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); } // At the bottom put time/GPS state/etc -lc.push({ type:"h", filly:1, c:[ +lc.push({ type:"h", id:"bottom", filly:1, c:[ {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } @@ -139,7 +187,7 @@ lc.push({ type:"h", filly:1, c:[ // Now calculate the layout let layout = new Layout( { type:"v", c: lc -},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); +},{lazy:true, btns:[{ label:"---", cb: onStartStop, id:"button"}]}); delete lc; setStatus(exs.state.active); layout.render(); @@ -169,45 +217,49 @@ Bangle.on("GPS", function(fix) { } }); -// run() function used to switch between traditional run UI and karvonen UI -function run() { - wu.show(); - layout.lazy = false; - layout.render(); - layout.lazy = true; - // We always call ourselves once a second to update - if (!runInterval){ - runInterval = setInterval(function() { - layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed && !karvonenActive) layout.render(); - }, 1000); +function setScreen(to) { + if (screen === "karvonen") { + require("runplus_karvonen").stop(); + wu.show(); + Bangle.drawWidgets(); + } + + if (runInterval) clearInterval(runInterval); + runInterval = undefined; + g.reset().clearRect(Bangle.appRect); + + screen = to; + switch (screen) { + case "main": + layout.lazy = false; + layout.render(); + layout.lazy = true; + // We always call ourselves once a second to update + if (!runInterval){ + runInterval = setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (screen !== "menu") layout.render(); + }, 1000); + } + break; + + case "karvonen": + require("runplus_karvonen").start(settings.HRM, exs.stats.bpm); + break; } } -run(); -/////////////////////////////////////////////// -// Karvonen -/////////////////////////////////////////////// - -function stopRunUI() { - // stop updating and drawing the traditional run app UI - clearInterval(runInterval); - runInterval = undefined; - karvonenActive = true; -} - -function stopKarvonenUI() { - g.reset().clear(); - clearInterval(karvonenInterval); - karvonenInterval = undefined; - karvonenActive = false; -} - -let karvonenInterval; // Define the function to go back and forth between the different UI's function swipeHandler(LR,_) { - if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} - if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} + if (screen !== "menu"){ + if (LR < 0 && screen == "karvonen") + setScreen("main"); + if (LR > 0 && screen !== "karvonen") + setScreen("karvonen"); // stop updating and drawing the traditional run app UI + } } + +setScreen("main"); + // Listen for swipes with the swipeHandler Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/karvonen.js b/apps/runplus/karvonen.js index de81494bb..afdbdb1cf 100644 --- a/apps/runplus/karvonen.js +++ b/apps/runplus/karvonen.js @@ -1,19 +1,19 @@ -(function karvonen(hrmSettings, exsHrmStats) { - //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ - //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. - //Other methods are even more approximative. - let wu = require("widget_utils"); - wu.hide(); - let R = Bangle.appRect; +//This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ +//The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. +//Other methods are even more approximative. +let wu = require("widget_utils"); +let R; - - g.reset().clearRect(R).setFontAlign(0,0,0); - - const x = "x"; const y = "y"; - function Rdiv(axis, divisor) { // Used when placing things on the screen - return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); - } - let linePoints = { //Not lists of points, but used to update points in the drawArrows function. +const ICON_LOCK = atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="); +const ICON_HEART = atob("Dw4BODj4+fv3///////f/z/+P/g/4D+APgA4ACAA"); +const ICON_LOCATION = atob("CxABP4/7x/B+D8H8e/5/x/D+D4HwHAEAIA=="); + +const x = "x"; const y = "y"; +function Rdiv(axis, divisor) { // Used when placing things on the screen + return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); +} + +let linePoints = { //Not lists of points, but used to update points in the drawArrows function. x: [ 175/40, 2, @@ -24,192 +24,324 @@ 175/52, 175/110, 175/122, - ], - - }; - - function drawArrows() { - g.setColor(g.theme.fg); - // Upper - g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); - g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0])); - // Lower - g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); - g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); - } - - //To calculate Heart rate zones, we need to know the heart rate reserve (HRR) - // HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. - //get the hrr (heart rate reserve). - // I put random data here, but this has to come as a menu in the settings section so that users can change it. - let minhr = hrmSettings.min; - let maxhr = hrmSettings.max; - - function calculatehrr(minhr, maxhr) { - return maxhr - minhr;} - - //test input for hrr (it works). - let hrr = calculatehrr(minhr, maxhr); - console.log(hrr); - - //Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. - let hr = exsHrmStats.getValue(); - let hr1 = hr; - // These letiables display next and previous HR zone. - //get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method - //60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack - let minzone2 = hrr * 0.6 + minhr; - let maxzone2 = hrr * 0.7 + minhr; - let maxzone3 = hrr * 0.8 + minhr; - let maxzone4 = hrr * 0.9 + minhr; - let maxzone5 = hrr * 0.99 + minhr; + ] +}; - // HR data: large, readable, in the middle of the screen - function drawHR() { - g.setFontAlign(-1,0,0); - g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); - g.setColor(g.theme.fg); - g.setFont("Vector",50); - g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); + +function drawArrows() { + g.setColor(g.theme.fg); + // Upper + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0])); + // Lower + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); +} + +//To calculate Heart rate zones, we need to know the heart rate reserve (HRR) +// HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. +//get the hrr (heart rate reserve). +// I put random data here, but this has to come as a menu in the settings section so that users can change it. +let minhr; +let maxhr; +let hrr; + +//Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. +let hr; +// These letiables display next and previous HR zone. +//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method +//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack +let minzone2; +let maxzone2; +let maxzone3; +let maxzone4; +let maxzone5; + +function calculatehrr(minhr, maxhr) { + hrr = maxhr - minhr; + minzone2 = hrr * 0.6 + minhr; + maxzone2 = hrr * 0.7 + minhr; + maxzone3 = hrr * 0.8 + minhr; + maxzone4 = hrr * 0.9 + minhr; + maxzone5 = hrr * 0.99 + minhr; +} +// HR data: large, readable, in the middle of the screen +function drawHR() { + g.setFontAlign(-1,0,0); + g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); + g.setColor(g.theme.fg); + g.setFont("Vector",50); + g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); + drawArrows(); +} + +function drawWaitHR() { + g.setColor(g.theme.fg); + // Waiting for HRM + g.setFontAlign(0,0,0); + g.setFont("Vector",50); + g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); + + // Waiting for current Zone + g.setFont("Vector",24); + g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); + + // waiting for upper and lower limit of current zone + g.setFont("Vector",20); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); + + drawArrows(); +} + +//These functions call arcs to show different HR zones. + +//To shorten the code, I'll reference some letiables and reuse them. +let centreX; +let centreY; +let minRadius; +let maxRadius; + +//draw background image (dithered green zones)(I should draw different zones in different dithered colors) +const HRzones= require("graphics_utils"); +let minRadiusz; +let startAngle; +let endAngle; + +function drawBgArc() { + g.setColor(g.theme.dark==false?0xC618:"#002200"); + HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); +} + +const zones = require("graphics_utils"); +//####### A function to simplify a bit the code ###### +function simplify (sA, eA, Z, currentZone, lastZone) { + let startAngle = zones.degreesToRadians(sA); + let endAngle = zones.degreesToRadians(eA); + if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); + else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + g.setFont("Vector",24); + g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); + g.setFontAlign(0,0,0); + g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); +} + +function zoning (max, min) { // draw values of upper and lower limit of current zone + g.setFont("Vector",20); + g.setColor(g.theme.fg); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); + g.setFontAlign(0,0,0); + g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); + g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); +} + +function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color + g.setColor(g.theme.bg); + HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); +} + +function drawZone(zone) { + clearCurrentZone(); + if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} + if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} + if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} + if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} + if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} + if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} + if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} + if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} + if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} + if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} + if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} + if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} + if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} } - function drawWaitHR() { - g.setColor(g.theme.fg); - // Waiting for HRM - g.setFontAlign(0,0,0); - g.setFont("Vector",50); - g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); +function drawIndicators(){ + drawLockIndicator(); + drawPulseIndicator(); + drawGpsIndicator(); +} - // Waiting for current Zone - g.setFont("Vector",24); - g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); +function drawZoneAlert() { + const HRzonemax = require("graphics_utils"); + drawIndicators(); + g.clearRect(R); + drawIndicators(); + let minRadius = 0.40 * R.h; + let startAngle1 = HRzonemax.degreesToRadians(-90); + let endAngle1 = HRzonemax.degreesToRadians(270); + g.setFont("Vector",38);g.setColor("#ff0000"); + HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); + g.setFontAlign(0,0).drawString("ALERT", centreX, centreY); +} - // waiting for upper and lower limit of current zone - g.setFont("Vector",20); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); +function drawWaitUI(){ + g.clearRect(R); + drawIndicators(); + drawBgArc(); + drawWaitHR(); + drawArrows(); +} + +function drawBase() { + g.clearRect(R); + drawIndicators(); + drawBgArc(); +} + +function drawZoneUI(full){ + if (full) { + drawBase(); } - - //These functions call arcs to show different HR zones. - - //To shorten the code, I'll reference some letiables and reuse them. - let centreX = R.x + 0.5 * R.w; - let centreY = R.y + 0.5 * R.h; - let minRadius = 0.38 * R.h; - let maxRadius = 0.50 * R.h; - - //draw background image (dithered green zones)(I should draw different zones in different dithered colors) - const HRzones= require("graphics_utils"); - let minRadiusz = 0.44 * R.h; - let startAngle = HRzones.degreesToRadians(-88.5); - let endAngle = HRzones.degreesToRadians(268.5); + drawHR(); + drawZones(); +} - function drawBgArc() { - g.setColor(g.theme.dark==false?0xC618:"#002200"); - HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); +//Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. +let subZoneLast; +function drawZones() { + if ((hr < maxhr - 2) && subZoneLast==13) { drawZoneUI(true); } // Reset UI when coming down from zone alert. + if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; drawZone(subZoneLast);}} // Z1 + else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; drawZone(subZoneLast);}} // Z2a + else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; drawZone(subZoneLast);}} // Z2b + else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; drawZone(subZoneLast);}} // Z2c + else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; drawZone(subZoneLast);}} // Z3a + else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; drawZone(subZoneLast);}} // Z3b + else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; drawZone(subZoneLast);}} // Z3c + else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; drawZone(subZoneLast);}} // Z4a + else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; drawZone(subZoneLast);}} // Z4b + else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; drawZone(subZoneLast);}} // Z4c + else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; drawZone(subZoneLast);}} // Z5a + else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; drawZone(subZoneLast);}} // Z5b + else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; drawZone(subZoneLast);}} // Z5c + else if (hr >= maxhr - 2) {subZoneLast=13; drawZoneAlert();} // Alert +} + +let karvonenInterval; +let hrmstat; + +function drawLockIndicator() { + if (Bangle.isLocked()) + g.setColor(g.theme.fg).drawImage(ICON_LOCK, 6, 8); + else + g.setColor(g.theme.bg).drawImage(ICON_LOCK, 6, 8); +} + +let gpsTimeout; +let lastGps; +function drawGpsIndicator(e) { + if (e && e.fix){ + if (gpsTimeout) + clearTimeout(gpsTimeout); + g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23); + lastGps = 0; + gpsTimeout = setTimeout(()=>{ + g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23); + lastGps = 1; + gpsTimeout = setTimeout(()=>{ + g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23); + lastGps = 2; + }, 3900); + }, 1100); + } else if (lastGps !== undefined){ + switch (lastGps) { + case 0: g.setColor(g.theme.fg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break; + case 1: g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_LOCATION, 8, R.y2 - 23); break; + case 2: g.setColor(g.theme.bg).drawImage(ICON_LOCATION, 8, R.y2 - 23); break; + } } - - const zones = require("graphics_utils"); - //####### A function to simplify a bit the code ###### - function simplify (sA, eA, Z, currentZone, lastZone) { - let startAngle = zones.degreesToRadians(sA); - let endAngle = zones.degreesToRadians(eA); - if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); - else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); - g.setFont("Vector",24); - g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); - g.setFontAlign(0,0,0); - g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); +} + +let pulseTimeout; +function drawPulseIndicator() { + if (hr){ + if (pulseTimeout) + clearTimeout(pulseTimeout); + g.setColor(g.theme.fg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21); + pulseTimeout = setTimeout(()=>{ + g.setColor(g.theme.dark?"#ccc":"#444").drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21); + pulseTimeout = setTimeout(()=>{ + g.setColor(g.theme.bg).drawImage(ICON_HEART, R.x2 - 21, R.y2 - 21); + }, 3900); + }, 1100); + } +} + +function init(hrmSettings, exsHrmStats) { + R = Bangle.appRect; + hrmstat = exsHrmStats; + hr = hrmstat.getValue(); + minhr = hrmSettings.min; + maxhr = hrmSettings.max; + calculatehrr(minhr, maxhr); + + centreX = R.x + 0.5 * R.w; + centreY = R.y + 0.5 * R.h; + minRadius = 0.38 * R.h; + maxRadius = 0.50 * R.h; + + minRadiusz = 0.44 * R.h; + startAngle = HRzones.degreesToRadians(-88.5); + endAngle = HRzones.degreesToRadians(268.5); +} + +function start(hrmSettings, exsHrmStats) { + wu.hide(); + init(hrmSettings, exsHrmStats); + + g.reset().clearRect(R).setFontAlign(0,0,0); + + Bangle.on("lock", drawLockIndicator); + Bangle.on("HRM", drawPulseIndicator); + Bangle.on("HRM", updateUI); + Bangle.on("GPS", drawGpsIndicator); + + setTimeout(updateUI,0,true); +} +let waitTimeout; +let hrLast; +//h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. +function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. + if (resetHrLast){ + hrLast = 0; // Handles correct updating on init depending on if we've got HRM readings yet or not. + subZoneLast = undefined; } - function zoning (max, min) { // draw values of upper and lower limit of current zone - g.setFont("Vector",20); - g.setColor(g.theme.fg); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); - g.setFontAlign(0,0,0); - g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); - g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); + if (waitTimeout) clearTimeout(waitTimeout); + waitTimeout = setTimeout(() => {drawWaitUI(); waitTimeout = undefined;}, 2000); + + hr = hrmstat.getValue(); + //if (h!=0) hr = h; + + if (hrLast != hr || resetHrLast){ + if (hr && hrLast != hr){ + drawZoneUI(true); + } else if (!hr) + drawWaitUI(); } - function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color - g.setColor(g.theme.bg); - HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); - } - - function getZone(zone) { - drawBgArc(); - clearCurrentZone(); - if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} - if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} - if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} - if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} - if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} - if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} - if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} - if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} - if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} - if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} - if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} - if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} - if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} - } + hrLast = hr; + //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. +} - function getZoneAlert() { - const HRzonemax = require("graphics_utils"); - let centreX1,centreY1,maxRadius1 = 1; - let minRadius = 0.40 * R.h; - let startAngle1 = HRzonemax.degreesToRadians(-90); - let endAngle1 = HRzonemax.degreesToRadians(270); - g.setFont("Vector",38);g.setColor("#ff0000"); - HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); - g.drawString("ALERT", 26,66); - } - - //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. - let subZoneLast; - function drawZones() { - if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. - if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 - else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a - else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b - else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c - else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a - else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b - else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c - else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a - else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b - else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c - else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a - else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b - else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c - else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert - } - - function initDraw() { - drawArrows(); - if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} - //drawZones(); - } +function stop(){ + if (karvonenInterval) clearInterval(karvonenInterval); + karvonenInterval = undefined; + if (pulseTimeout) clearTimeout(pulseTimeout); + pulseTimeout = undefined; + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + if (waitTimeout) clearTimeout(waitTimeout); + waitTimeout = undefined; + Bangle.removeListener("lock", drawLockIndicator); + Bangle.removeListener("GPS", drawGpsIndicator); + Bangle.removeListener("HRM", drawPulseIndicator); + Bangle.removeListener("HRM", updateUI); + lastGps = undefined; + wu.show(); +} - let hrLast; - //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. - function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. - hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. - hr = exsHrmStats.getValue(); - //if (h!=0) hr = h; - if (hr!=hrLast) { - drawHR(); - drawZones(); - } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. - } - - initDraw(); - - // check for updates every second. - karvonenInterval = setInterval(function() { - if (!isMenuDisplayed && karvonenActive) updateUI(); - }, 1000); - - return karvonenInterval; -}) +exports.start = start; +exports.stop = stop; \ No newline at end of file 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/runplus/metadata.json b/apps/runplus/metadata.json index 16c6101ca..fe59050db 100644 --- a/apps/runplus/metadata.json +++ b/apps/runplus/metadata.json @@ -1,12 +1,15 @@ { "id": "runplus", "name": "Run+", - "version": "0.22", - "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", + "version": "0.27", + "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screens for heart rate interval training and individual stat focus.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", "supports": ["BANGLEJS2"], - "screenshots": [{"url": "screenshot.png"}], + "screenshots": [ + {"url": "screenshot.png"}, + {"url": "karvonen.png"} + ], "readme": "README.md", "storage": [ {"name": "runplus.app.js", "url": "app.js"}, diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js index 539391a27..77343d6b3 100644 --- a/apps/runplus/settings.js +++ b/apps/runplus/settings.js @@ -17,6 +17,7 @@ B5: "step", B6: "caden", paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale + alwaysResume: false, notify: { dist: { increment: 0, @@ -72,6 +73,13 @@ saveSettings(); } }; + menu[/*LANG*/"Always resume run"] = { + value : settings.alwaysResume, + onchange : v => { + settings.alwaysResume = v; + saveSettings(); + }, + }; var notificationsMenu = { '< Back': function() { E.showMenu(menu) }, } diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog index 7a1d5db21..5e12b9562 100644 --- a/apps/ruuviwatch/ChangeLog +++ b/apps/ruuviwatch/ChangeLog @@ -1,4 +1,6 @@ 0.01: Hello Ruuvi Watch! 0.02: Clear gfx on startup. 0.03: Improve design and code, reduce flicker. -0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale. \ No newline at end of file +0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale. +0.05: Minor code improvements +0.06: Minor code improvements diff --git a/apps/ruuviwatch/metadata.json b/apps/ruuviwatch/metadata.json index eab9f64bf..1eee35c62 100644 --- a/apps/ruuviwatch/metadata.json +++ b/apps/ruuviwatch/metadata.json @@ -2,7 +2,7 @@ "name": "Ruuvi Watch", "shortName":"Ruuvi Watch", "icon": "ruuviwatch.png", - "version":"0.04", + "version": "0.06", "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.", "readme":"README.md", "tags": "bluetooth", diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js index 249303382..be4855539 100644 --- a/apps/ruuviwatch/ruuviwatch.app.js +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -16,9 +16,9 @@ let paused = false; const SCAN_FREQ = 1000 * 30; // ALERT LIMITS -LIMIT_SAUNA = 60; -LIMIT_FRIDGE = 4; -LIMIT_FREEZER = -18; +const LIMIT_SAUNA = 60; +const LIMIT_FRIDGE = 4; +const LIMIT_FREEZER = -18; // TODO add wine cellar limits // TODO configurable limits @@ -486,6 +486,6 @@ g.drawImage( g.setFont(FONT_M); g.drawString("Ruuvi Watch", CENTER, HUMID_PRESSURE_Y); -var ageInterval = setInterval(redrawAge, 1000); -var scanInterval = setInterval(scan, SCAN_FREQ); +setInterval(redrawAge, 1000); +setInterval(scan, SCAN_FREQ); scan(); diff --git a/apps/sched/ChangeLog b/apps/sched/ChangeLog index 92b04fb32..1b385c8ea 100644 --- a/apps/sched/ChangeLog +++ b/apps/sched/ChangeLog @@ -23,3 +23,7 @@ 0.20: Alarm dismiss and snooze events 0.21: Fix crash in clock_info 0.22: Dated event repeat option +0.23: Allow buzzing forever when an alarm fires +0.24: Emit alarmReload when alarms change (used by widalarm) +0.25: Fix wrap around when snoozed through midnight +0.26: Fix hitting snooze on an alarm after when the snooze would've fired diff --git a/apps/sched/README.md b/apps/sched/README.md index 2fb201cee..1216a1a11 100644 --- a/apps/sched/README.md +++ b/apps/sched/README.md @@ -14,10 +14,10 @@ Global Settings --------------- - `Unlock at Buzz` - If `Yes` the alarm/timer will unlock the watch +- `Delete Expired Timers` - Default for whether expired timers are removed after firing. - `Default Auto Snooze` - Default _Auto Snooze_ value for newly created alarms (_Alarms_ only) - `Default Snooze` - Default _Snooze_ value for newly created alarms/timers -- `Default Repeat` - Default _Repeat_ value for newly created alarms (_Alarms_ only) -- `Buzz Count` - The number of buzzes before the watch goes silent +- `Buzz Count` - The number of buzzes before the watch goes silent, or "forever" to buzz until stopped. - `Buzz Interval` - The interval between one buzz and the next - `Default Alarm/Timer Pattern` - Default vibration pattern for newly created alarms/timers diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 62e45676b..73ceff3c1 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -2,6 +2,26 @@ + +
Loading...
+ + + diff --git a/apps/timestamplog/lib.js b/apps/timestamplog/lib.js new file mode 100644 index 000000000..59590bfb7 --- /dev/null +++ b/apps/timestamplog/lib.js @@ -0,0 +1,248 @@ +const storage = require('Storage'); + +// Storage filenames + +const LOG_FILENAME = 'timestamplog.json'; +const SETTINGS_FILENAME = 'timestamplog.settings.json'; + + +// Settings + +const SETTINGS = Object.assign({ + logFont: '12x20', + logFontHSize: 1, + logFontVSize: 1, + maxLogLength: 30, + rotateLog: false, + buttonAction: 'Log time', +}, storage.readJSON(SETTINGS_FILENAME, true) || {}); + +const SETTINGS_BUTTON_ACTION = [ + 'Log time', + 'Open settings', + 'Quit app', + 'Do nothing', +]; + + +function fontSpec(name, hsize, vsize) { + return name + ':' + hsize + 'x' + vsize; +} + + +//// Data models //// + +// High-level timestamp log object that provides an interface to the +// UI for managing log entries and automatically loading/saving +// changes to flash storage. +class StampLog { + constructor(filename, maxLength) { + // Name of file to save log to + this.filename = filename; + // Maximum entries for log before old entries are overwritten with + // newer ones + this.maxLength = maxLength; + + // `true` when we have changes that need to be saved + this.isDirty = false; + // Wait at most this many msec upon first data change before + // saving (this is to avoid excessive writes to flash if several + // changes happen quickly; we wait a little bit so they can be + // rolled into a single write) + this.saveTimeout = 30000; + // setTimeout ID for scheduled save job + this.saveId = null; + // Underlying raw log data object. Outside this class it's + // recommended to use only the class methods to change it rather + // than modifying the object directly to ensure that changes are + // recognized and saved to storage. + this.log = []; + + this.load(); + } + + // Read in the log data that is currently in storage + load() { + let log = storage.readJSON(this.filename, true); + if (!log) log = []; + // Convert stringified datetimes back into Date objects + for (let logEntry of log) { + logEntry.stamp = new Date(logEntry.stamp); + } + this.log = log; + } + + // Write current log data to storage if anything needs to be saved + save() { + // Cancel any pending scheduled calls to save() + if (this.saveId) { + clearTimeout(this.saveId); + this.saveId = null; + } + + if (this.isDirty) { + let logToSave = []; + for (let logEntry of this.log) { + // Serialize each Date object into an ISO string before saving + let newEntry = Object.assign({}, logEntry); + newEntry.stamp = logEntry.stamp.toISOString(); + logToSave.push(newEntry); + } + + if (storage.writeJSON(this.filename, logToSave)) { + console.log('stamplog: save to storage completed'); + this.isDirty = false; + } else { + console.log('stamplog: save to storage FAILED'); + this.emit('saveError'); + } + } else { + console.log('stamplog: skipping save to storage because no changes made'); + } + } + + // Mark log as needing to be (re)written to storage + setDirty() { + this.isDirty = true; + if (!this.saveId) { + this.saveId = setTimeout(this.save.bind(this), this.saveTimeout); + } + } + + // Add a timestamp for the current time to the end of the log + addEntry() { + // If log full, purge an old entry to make room for new one + if (this.maxLength) { + while (this.log.length + 1 > this.maxLength) { + this.log.shift(); + } + } + // Add new entry + this.log.push({ + stamp: new Date() + }); + this.setDirty(); + } + + // Delete the log objects given in the array `entries` from the log + deleteEntries(entries) { + this.log = this.log.filter(entry => !entries.includes(entry)); + this.setDirty(); + } + + // Does the log currently contain the maximum possible number of entries? + isFull() { + return this.log.length >= this.maxLength; + } +} + +function launchSettingsMenu(backCb) { + const fonts = g.getFonts(); + const stampLog = new StampLog(LOG_FILENAME, SETTINGS.maxLogLength); + + function saveSettings() { + console.log('Saving timestamp log and settings'); + stampLog.save(); + if (!storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { + E.showAlert('Trouble saving settings'); + } + } + E.on('kill', saveSettings); + + function endMenu() { + saveSettings(); + E.removeListener('kill', saveSettings); + backCb(); + } + + function topMenu() { + E.showMenu({ + '': { + title: 'Stamplog', + back: endMenu, + }, + 'Log': logMenu, + 'Appearance': appearanceMenu, + 'Button': { + value: SETTINGS_BUTTON_ACTION.indexOf(SETTINGS.buttonAction), + min: 0, max: SETTINGS_BUTTON_ACTION.length - 1, + format: v => SETTINGS_BUTTON_ACTION[v], + onchange: v => { + SETTINGS.buttonAction = SETTINGS_BUTTON_ACTION[v]; + }, + }, + }); + } + + function logMenu() { + E.showMenu({ + '': { + title: 'Log', + back: topMenu, + }, + 'Max entries': { + value: SETTINGS.maxLogLength, + min: 5, max: 100, step: 5, + onchange: v => { + SETTINGS.maxLogLength = v; + stampLog.maxLength = v; + } + }, + 'Auto-delete oldest': { + value: SETTINGS.rotateLog, + onchange: v => { + SETTINGS.rotateLog = !SETTINGS.rotateLog; + } + }, + 'Clear log': doClearLog, + }); + } + + function appearanceMenu() { + E.showMenu({ + '': { + title: 'Appearance', + back: topMenu, + }, + 'Log font': { + value: fonts.indexOf(SETTINGS.logFont), + min: 0, max: fonts.length - 1, + format: v => fonts[v], + onchange: v => { + SETTINGS.logFont = fonts[v]; + }, + }, + 'Log font H size': { + value: SETTINGS.logFontHSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontHSize = v; + }, + }, + 'Log font V size': { + value: SETTINGS.logFontVSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontVSize = v; + }, + }, + }); + } + + function doClearLog() { + E.showPrompt('Erase ALL log entries?', { + title: 'Clear log', + buttons: {'Erase':1, "Don't":0} + }).then((yes) => { + if (yes) { + stampLog.deleteEntries(stampLog.log); + } + logMenu(); + }); + } + + topMenu(); +} + +exports = {LOG_FILENAME, SETTINGS_FILENAME, SETTINGS, SETTINGS_BUTTON_ACTION, fontSpec, StampLog, + launchSettingsMenu}; diff --git a/apps/timestamplog/metadata.json b/apps/timestamplog/metadata.json new file mode 100644 index 000000000..e1aa0eb23 --- /dev/null +++ b/apps/timestamplog/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "timestamplog", + "name": "Timestamp log", + "shortName":"Timestamp log", + "icon": "app.png", + "version": "0.01", + "description": "Conveniently record a series of date/time stamps", + "screenshots": [ {"url": "screenshot.png" } ], + "readme": "README.md", + "tags": "timestamp, log", + "supports": ["BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name": "timestamplog.app.js", "url": "app.js"}, + {"name": "timestamplog.img", "url": "app-icon.js", "evaluate": true}, + {"name": "timestamplog", "url": "lib.js"}, + {"name": "timestamplog.settings.js", "url": "settings.js"} + ], + "data": [ + {"name": "timestamplog.settings"}, + {"name": "timestamplog.json"} + ] +} diff --git a/apps/timestamplog/screenshot.png b/apps/timestamplog/screenshot.png new file mode 100644 index 000000000..27f36ac32 Binary files /dev/null and b/apps/timestamplog/screenshot.png differ diff --git a/apps/timestamplog/settings.js b/apps/timestamplog/settings.js new file mode 100644 index 000000000..137ed31db --- /dev/null +++ b/apps/timestamplog/settings.js @@ -0,0 +1,7 @@ +const tsl = require('timestamplog'); + +( + function(backCb) { + tsl.launchSettingsMenu(backCb); + } +); diff --git a/apps/tinyVario/ChangeLog b/apps/tinyVario/ChangeLog index a201ee465..437eebe73 100644 --- a/apps/tinyVario/ChangeLog +++ b/apps/tinyVario/ChangeLog @@ -3,3 +3,5 @@ 0.03: Changed menu layout, fixed automatic flight time detection. 0.04: flight time detection should work without GPS now. New vario display available. 0.05: some bugs fiexed, no new features. +0.06: Minor code improvements +0.07: Minor code improvements diff --git a/apps/tinyVario/app.js b/apps/tinyVario/app.js index b9a87c821..87ed671be 100644 --- a/apps/tinyVario/app.js +++ b/apps/tinyVario/app.js @@ -5,12 +5,12 @@ To do: -navigation */ -getAltitude = (p,baseP) => (44330 * (1.0 - Math.pow(p/baseP, 0.1903))); -getFL = () => (44330 * (1.0 - Math.pow(pressure/1013.25, 0.1903))).toFixed(0); -getTimeString = () => (settings.localTime) ? (require("locale").time(Date(),1)):(Date().toUTCString().slice(Date().toUTCString().length-12,Date().toUTCString().length-7)); +const getAltitude = (p,baseP) => (44330 * (1.0 - Math.pow(p/baseP, 0.1903))); +//const getFL = () => (44330 * (1.0 - Math.pow(pressure/1013.25, 0.1903))).toFixed(0); +const getTimeString = () => (settings.localTime) ? (require("locale").time(Date(),1)):(Date().toUTCString().slice(Date().toUTCString().length-12,Date().toUTCString().length-7)); var fg=g.getColor(); -var bg=g.getBgColor(); +//var bg=g.getBgColor(); var red="#F00",green="#0F0"; const unitsRoc=[ @@ -40,11 +40,13 @@ const unitsAlt=[ {name:"m", factor:1, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"m"}}, {name:"ft", factor:3.280839895013123, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"ft"}} ]; +/* const unitROC={type:"v", halign:1, c: [ {type:"txt", font:"12%", halign:0, filly:0, label:"m"}, {type:"", height:1,width:"20", bgCol:fg}, {type:"txt", font:"12%", halign:0, filly:0, label:"s"} ]}; +*/ const ground=0, flying=1, landed=2, maybeFlying=3, maybeLanded=4; @@ -65,7 +67,7 @@ var altH = []; var altRaw=-9999, altFast=0, altSlow=0; var fastGain=0.5, slowGain=0.3; var roc=0,rocAvg=0, gs; -var lastPressure = Date.now(); +//var lastPressure = Date.now(); var pressure = 1000; var state=ground; var takeoffTime=0, landingTime=0, flyingTime; @@ -144,7 +146,7 @@ function initPFD() { //samples=1; //-------------------- pfdHandle = setInterval(function() { - t1=Date().getTime(); + //const t1=Date().getTime(); //process pressure readings if (samples) { pressure=rawP/samples; @@ -224,7 +226,7 @@ function initPFD() { } function initAltMenu() { - var oldQnh=qnh; + //var oldQnh=qnh; function updateAltMenu() { altMenu.clear(); altMenu.alt.label= diff --git a/apps/tinyVario/metadata.json b/apps/tinyVario/metadata.json index f038e7515..866d820f9 100644 --- a/apps/tinyVario/metadata.json +++ b/apps/tinyVario/metadata.json @@ -1,7 +1,7 @@ { "id": "tinyVario", "name": "Tiny Vario", "shortName" : "tinyVario", - "version":"0.05", + "version": "0.07", "icon": "app.png", "readme": "README.md", "description": "A very simple app for gliding / paragliding / hang gliding etc.", diff --git a/apps/tinydraw/ChangeLog b/apps/tinydraw/ChangeLog index 4bae1b9f8..6d7c7cfc2 100644 --- a/apps/tinydraw/ChangeLog +++ b/apps/tinydraw/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial release 0.02: Don't start drawing with white colour on white canvas 0.03: Fix segmented line glitch when drawing, optimize screen lock detection +0.04: Minor code improvements diff --git a/apps/tinydraw/app.js b/apps/tinydraw/app.js index 52460bc79..5ba3ed50c 100644 --- a/apps/tinydraw/app.js +++ b/apps/tinydraw/app.js @@ -164,14 +164,14 @@ Bangle.on("lock", function() { case 'circle': var XS = (to.x - from.x) / 32; var YS = (to.y - from.y) / 32; - for (i = 0; i < 32; i++) { + for (let i = 0; i < 32; i++) { g.fillCircle(from.x + (i * XS), from.y + (i * YS), 4, 4); } break; case 'square': var XS = (to.x - from.x) / 32; var YS = (to.y - from.y) / 32; - for (i = 0; i < 32; i++) { + for (let i = 0; i < 32; i++) { const posX = from.x + (i * XS); const posY = from.y + (i * YS); g.fillRect(posX - 10, posY - 10, posX + 10, posY + 10); diff --git a/apps/tinydraw/metadata.json b/apps/tinydraw/metadata.json index 35d994ec3..21ea1eb89 100644 --- a/apps/tinydraw/metadata.json +++ b/apps/tinydraw/metadata.json @@ -1,7 +1,7 @@ { "id": "tinydraw", "name": "TinyDraw", "shortName":"TinyDraw", - "version":"0.03", + "version": "0.04", "type": "app", "description": "Draw stuff in your wrist", "icon": "app.png", diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index e15ffa29b..a309b4f7f 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -6,3 +6,4 @@ 0.06: Complete rewrite in 80x80, better perf, add settings 0.07: Added suppport for Bangle 2, added README file 0.08: Use default Bangle formatter for booleans +0.09: Minor code improvements diff --git a/apps/toucher/app.js b/apps/toucher/app.js index 19310592e..b45725aab 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -85,7 +85,7 @@ function render(){ const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF ); let cycle = 0; - let lastCycle = visibleApps.length; + //let lastCycle = visibleApps.length; visibleApps.forEach(app => { cycle++; diff --git a/apps/toucher/metadata.json b/apps/toucher/metadata.json index 0c7a35773..9c4cd9678 100644 --- a/apps/toucher/metadata.json +++ b/apps/toucher/metadata.json @@ -2,7 +2,7 @@ "id": "toucher", "name": "Touch Launcher", "shortName": "Toucher", - "version": "0.08", + "version": "0.09", "description": "Touch enable left to right launcher.", "icon": "app.png", "type": "launch", diff --git a/apps/trex/ChangeLog b/apps/trex/ChangeLog index a92abb0df..b89bb7af4 100644 --- a/apps/trex/ChangeLog +++ b/apps/trex/ChangeLog @@ -1,3 +1,4 @@ 0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.03: Enabled BTN2 and BTN3, added highscore (score is saved to storage and can be reset in app settings menu) 0.04: Bangle.js 2 support +0.05: Minor code improvements diff --git a/apps/trex/metadata.json b/apps/trex/metadata.json index 8344ba161..6e2f3f567 100644 --- a/apps/trex/metadata.json +++ b/apps/trex/metadata.json @@ -1,7 +1,7 @@ { "id": "trex", "name": "T-Rex", - "version": "0.04", + "version": "0.05", "description": "T-Rex game in the style of Chrome's offline game", "icon": "trex.png", "screenshots": [{"url":"screenshot_trex.png"}], diff --git a/apps/trex/trex.js b/apps/trex/trex.js index 3baca101b..543a6dcae 100644 --- a/apps/trex/trex.js +++ b/apps/trex/trex.js @@ -19,7 +19,7 @@ if (process.env.HWVERSION==2) { BTNR = { read : _=>tap.b && tap.x > 88}; // use button for jump BTNU = BTN1; - greal = g; + const greal = g; g = Graphics.createArrayBuffer(88,64,1,{msb:true}); g.flip = function() { greal.drawImage({ @@ -33,7 +33,7 @@ if (process.env.HWVERSION==2) { BTNL = BTN2; BTNR = BTN3; BTNU = BTN1; - greal = g; + const greal = g; g = Graphics.createArrayBuffer(120,64,1,{msb:true}); g.flip = function() { greal.drawImage({ diff --git a/apps/twenties/metadata.json b/apps/twenties/metadata.json index b1dfe2134..dcbe0b565 100644 --- a/apps/twenties/metadata.json +++ b/apps/twenties/metadata.json @@ -6,7 +6,7 @@ "description": "Buzzes every 20m to stand / sit and look 20ft away for 20s.", "icon": "app.png", "type": "bootloader", - "tags": "alarm,tool", + "tags": "alarm,tool,health", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [{ "name": "twenties.boot.js", "url": "boot.js" }] 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/usgs/ChangeLog b/apps/usgs/ChangeLog index 65536966a..2c66d9b70 100644 --- a/apps/usgs/ChangeLog +++ b/apps/usgs/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Changed menu names, added button interaction. +0.03: Minor code improvements diff --git a/apps/usgs/app.js b/apps/usgs/app.js index e6f2efa1e..90509284e 100644 --- a/apps/usgs/app.js +++ b/apps/usgs/app.js @@ -8,7 +8,7 @@ var FILE = "usgs.json"; tempUnitF: true, }, require('Storage').readJSON(FILE, true) || {}); function fetchStartup() { - uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + + const uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + settings.loc + "')/Datastreams?$expand=Observations($orderby=phenomenonTime%20desc;$top=1;$select=result)&$select=unitOfMeasurement,description"; if (Bangle.http) { @@ -17,8 +17,10 @@ function fetchStartup() { } function handleStartup(data) { for (var key1 in data) { - desc = data[key1].description.split(" / ")[0]; + const desc = data[key1].description.split(" / ")[0]; if (settings.keys[desc]) { + let symbol; + let result; if (data[key1].unitOfMeasurement.symbol === "degC" && settings.tempUnitF) { symbol = "F"; result = (data[key1].Observations[0].result * 9 / 5) + 32; @@ -42,10 +44,11 @@ function displayData(dataStreams) { g.clear(); g.setFont("Vector",20); g.setFontAlign(0,0); - string = ""; + let string = ""; for (var key in dataStreams) { - unit = dataStreams[key].unit; - value = dataStreams[key].value; + const unit = dataStreams[key].unit; + const value = dataStreams[key].value; + let name; if (settings.shortenedName[key]) { name = settings.shortenedName[key]; } else { diff --git a/apps/usgs/metadata.json b/apps/usgs/metadata.json index 6140c59b9..31d518fec 100644 --- a/apps/usgs/metadata.json +++ b/apps/usgs/metadata.json @@ -1,7 +1,7 @@ { "id": "usgs", "name": "USGS Data fetching app", "shortName":"USGS", - "version":"0.02", + "version": "0.03", "description": "App that fetches [USGS water data](https://maps.waterdata.usgs.gov/) for a configurable location (requires connection to Android phone)", "icon": "app.png", "tags": "outdoors,exercise,http", diff --git a/apps/usgs/settings.js b/apps/usgs/settings.js index 1ddde4c60..8a12197dc 100644 --- a/apps/usgs/settings.js +++ b/apps/usgs/settings.js @@ -68,7 +68,7 @@ function popSubMenuData(data) { function getDataStreams() { - uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + + const uri = "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-" + settings.loc + "')/Datastreams?$select=description"; if (Bangle.http) { diff --git a/apps/vernierrespirate/ChangeLog b/apps/vernierrespirate/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/vernierrespirate/ChangeLog +++ b/apps/vernierrespirate/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/vernierrespirate/app.js b/apps/vernierrespirate/app.js index 945b72b77..b2afdb0df 100644 --- a/apps/vernierrespirate/app.js +++ b/apps/vernierrespirate/app.js @@ -154,7 +154,7 @@ function connect() { offs += 4; } } else { - var cmd = dv.getUint8(4); // cmd + /*var cmd =*/ dv.getUint8(4); // cmd //print("CMD",dv.buffer); } } diff --git a/apps/vernierrespirate/metadata.json b/apps/vernierrespirate/metadata.json index 5e2baf2bb..517b95e33 100644 --- a/apps/vernierrespirate/metadata.json +++ b/apps/vernierrespirate/metadata.json @@ -2,7 +2,7 @@ "id": "vernierrespirate", "name": "Vernier Go Direct Respiration Belt", "shortName": "Respiration Belt", - "version": "0.01", + "version": "0.02", "description": "Connects to a Go Direct Respiration Belt and shows respiration rate", "icon": "app.png", "tags": "health,bluetooth", diff --git a/apps/viewstl/ChangeLog b/apps/viewstl/ChangeLog index fa2dcb2e1..4a49d85e7 100644 --- a/apps/viewstl/ChangeLog +++ b/apps/viewstl/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app! 0.02: Add accelerometer/compass viewing mode +0.03: Minor code improvements diff --git a/apps/viewstl/metadata.json b/apps/viewstl/metadata.json index 8d1cc3f98..8de658a95 100644 --- a/apps/viewstl/metadata.json +++ b/apps/viewstl/metadata.json @@ -2,7 +2,7 @@ "id": "viewstl", "name": "STL file viewer", "shortName": "ViewSTL", - "version": "0.02", + "version": "0.03", "description": "This app allows you to view STL 3D models on your watch", "icon": "icons8-octahedron-48.png", "tags": "tool", diff --git a/apps/viewstl/viewstl.app.js b/apps/viewstl/viewstl.app.js index 34d018705..440745603 100644 --- a/apps/viewstl/viewstl.app.js +++ b/apps/viewstl/viewstl.app.js @@ -222,7 +222,6 @@ function readSTL(fn) { edges = new Uint8Array(Math.max(faces.length/3,24)) p_edges = E.getAddressOf(edges, true); var fp=0, p=0; - var nf = 0; g.setColor(0.9, 0.9, 0.9); g.drawRect(20, 140, 220, 160); g.setColor(0.6, 0.6, 0.9); diff --git a/apps/viewstl/viewstl.min.js b/apps/viewstl/viewstl.min.js index 82975bbf9..1994c2f59 100644 --- a/apps/viewstl/viewstl.min.js +++ b/apps/viewstl/viewstl.min.js @@ -84,7 +84,6 @@ function readSTL(fn) { edges = new Uint8Array(Math.max(faces.length/3,24)) p_edges = E.getAddressOf(edges, true); var fp=0, p=0; - var nf = 0; g.setColor(0.9, 0.9, 0.9); g.drawRect(20, 140, 220, 160); g.setColor(0.6, 0.6, 0.9); 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/apps/walkersclock/ChangeLog b/apps/walkersclock/ChangeLog index 57404ac41..c0ebde89c 100644 --- a/apps/walkersclock/ChangeLog +++ b/apps/walkersclock/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fixed screen flicker 0.03: Added display of GPS fix lat/lon and course 0.04: Don't buzz for GPS fix in Quiet Mode +0.05: Minor code improvements diff --git a/apps/walkersclock/app.js b/apps/walkersclock/app.js index 8a5e826c4..f78be61ca 100644 --- a/apps/walkersclock/app.js +++ b/apps/walkersclock/app.js @@ -8,7 +8,7 @@ * - two function menus at present * GPS Power = On/Off * GPS Display = Grid | Speed Alt - * when the modeline in CYAN use button BTN1 to switch between options + * when the modeline in CYAN use button BTN1 to switch between options * - display the current steps if one of the steps widgets is installed * - ensures that BTN2 requires a 1.5 second press in order to switch to the launcher * this is so you dont accidently switch out of the GPS/watch display with you coat sleeve @@ -49,7 +49,6 @@ let infoMode = INFO_NONE; let functionMode = FN_MODE_OFF; let gpsDisplay = GDISP_OS; let prevInfoStr = "clear"; -let prevActivityStr = "clear"; let prevSteps = "clear"; let clearActivityArea = true; @@ -66,14 +65,14 @@ let last_fix = { satellites: 0 }; -function drawTime() { +function drawTime() { var d = new Date(); var da = d.toString().split(" "); var time = da[4].substr(0,5); g.reset(); g.clearRect(0,Y_TIME, 239, Y_ACTIVITY - 1); - + g.setColor(1,1,1); // white g.setFontAlign(0, -1); @@ -84,7 +83,7 @@ function drawTime() { } else { g.setFont("Vector", 80); } - + g.drawString(time, g.getWidth()/2, Y_TIME); } @@ -94,19 +93,19 @@ function drawActivity() { clearActivityArea = true; prevSteps = steps; - + if (clearActivityArea) { g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); clearActivityArea = false; } - + if (!gpsPowerState) { g.setColor(0,255,0); // green g.setFont("Vector", 60); g.drawString(getSteps(), g.getWidth()/2, Y_ACTIVITY); return; } - + g.setFont("6x8", 3); g.setColor(1,1,1); g.setFontAlign(0, -1); @@ -131,10 +130,10 @@ function drawActivity() { let ref = to_map_ref(6, os.easting, os.northing); let speed; let activityStr = ""; - + if (age < 0) age = 0; g.setFontVector(40); - g.setColor(0xFFC0); + g.setColor(0xFFC0); switch(gpsDisplay) { case GDISP_OS: @@ -147,7 +146,7 @@ function drawActivity() { case GDISP_SPEED: speed = last_fix.speed; speed = speed.toFixed(1); - activityStr = speed + "kph"; + activityStr = speed + "kph"; break; case GDISP_ALT: activityStr = last_fix.alt + "m"; @@ -160,7 +159,7 @@ function drawActivity() { g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); g.drawString(activityStr, 120, Y_ACTIVITY); g.setFont("6x8",2); - g.setColor(1,1,1); + g.setColor(1,1,1); g.drawString(age, 120, Y_ACTIVITY + 46); } } @@ -168,7 +167,7 @@ function drawActivity() { function onTick() { if (!Bangle.isLCDOn()) return; - + if (gpsPowerState) { drawAll(); return; @@ -194,7 +193,6 @@ function drawAll(){ } function drawInfo() { - let val; let str = ""; let col = 0x07E0; // green @@ -228,7 +226,7 @@ function drawInfo() { drawModeLine(str,col); return; } - + switch(infoMode) { case INFO_NONE: col = 0x0000; @@ -241,7 +239,7 @@ function drawInfo() { default: str = "Battery: " + E.getBattery() + "%"; } - + drawModeLine(str,col); } @@ -285,7 +283,7 @@ function changeInfoMode() { infoMode = INFO_NONE; clearActivityArea = true; return; - + case FN_MODE_GDISP: switch (gpsDisplay) { case GDISP_OS: @@ -306,7 +304,7 @@ function changeInfoMode() { break; } } - + switch(infoMode) { case INFO_NONE: if (stepsWidget() !== undefined) @@ -321,7 +319,7 @@ function changeInfoMode() { default: infoMode = INFO_NONE; } - + clearActivityArea = true; } @@ -353,7 +351,7 @@ function changeFunctionMode() { break; } } - + infoMode = INFO_NONE; // function mode overrides info mode } @@ -376,7 +374,7 @@ function processFix(fix) { gpsState = GPS_SATS; clearActivityArea = true; } - + if (fix.fix) { if (!last_fix.fix) { if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { @@ -403,7 +401,7 @@ function stepsWidget() { } return undefined; } - + /************* GPS / OSREF Code **************************/ @@ -415,10 +413,10 @@ function formatTime(now) { function timeSince(t) { var hms = t.split(":"); var now = new Date(); - + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - + return (sn - st); } @@ -485,7 +483,7 @@ OsGridRef.latLongToOsGrid = function(point) { * */ function to_map_ref(digits, easting, northing) { - if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing + if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); let e = easting; let n = northing; @@ -565,7 +563,7 @@ Bangle.on('lcdPower',function(on) { } }); -var click = setInterval(onTick, 5000); +setInterval(onTick, 5000); setWatch(() => { changeInfoMode(); drawAll(); }, BTN1, {repeat: true}); setWatch(() => { changeFunctionMode(); drawAll(); }, BTN3, {repeat: true}); diff --git a/apps/walkersclock/metadata.json b/apps/walkersclock/metadata.json index 3d93cffdf..ad40033f7 100644 --- a/apps/walkersclock/metadata.json +++ b/apps/walkersclock/metadata.json @@ -2,7 +2,7 @@ "id": "walkersclock", "name": "Walkers Clock", "shortName": "Walkers Clock", - "version": "0.04", + "version": "0.05", "description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference", "icon": "walkersclock48.png", "type": "clock", diff --git a/apps/warpdrive/ChangeLog b/apps/warpdrive/ChangeLog new file mode 100644 index 000000000..03c748ea5 --- /dev/null +++ b/apps/warpdrive/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Minor code improvements +0.03: Minor code improvements diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 855b86c78..b16766f47 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -496,7 +496,6 @@ let lcdBuffer = 0, let locked = false; let charging = false; let stopped = true; -let interval = 30; let timeout; function setupInterval(force) { @@ -573,7 +572,7 @@ function probe() { stride = 68; } */ - stride = 68; + let stride = 68; lcdBuffer = start; print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride); @@ -596,7 +595,6 @@ function init() { sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1); // setup nodes - let o = 0; for (let i = 0; i < nodeCount; ++i) { nodes[i] = { rx: 0, diff --git a/apps/warpdrive/metadata.json b/apps/warpdrive/metadata.json index c4fa1277e..330166bc8 100644 --- a/apps/warpdrive/metadata.json +++ b/apps/warpdrive/metadata.json @@ -1,7 +1,7 @@ { "id": "warpdrive", "name": "warpdrive clock", - "version": "0.01", + "version": "0.03", "description": "A watchface with an animated 3D scene.", "readme": "README.md", "icon": "app.png", diff --git a/apps/waternet/ChangeLog b/apps/waternet/ChangeLog index 7c9a36687..6ba406a6c 100644 --- a/apps/waternet/ChangeLog +++ b/apps/waternet/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version of Waternet port for Bangle JS 2 -0.02: Bigger play field tiles (more readable on watch), Always run without widgets visible \ No newline at end of file +0.02: Bigger play field tiles (more readable on watch), Always run without widgets visible +0.03: Minor code improvements diff --git a/apps/waternet/app.js b/apps/waternet/app.js index bc4b26828..cf2335009 100644 --- a/apps/waternet/app.js +++ b/apps/waternet/app.js @@ -94,7 +94,6 @@ const LEFTMENU = 118; const EMPTY = 61; let startPos; -let menuPos; let maxLevel; let selectedLevel; let boardX; @@ -133,7 +132,7 @@ let levelLocks = new Uint8Array(GMCOUNT * DIFFCOUNT); let options = new Uint8Array(OPCOUNT); //sound -let soundon = 1; +let soundon = 1; // TODO: Should this be 'soundOn' ? //game let paused; @@ -533,14 +532,18 @@ function set_bkg_data(tiles) { currentTiles = tiles; } +/* function get_bkg_data() { return currentTiles; } +*/ +/* function set_bkg_tiles(x, y, map) { "RAM"; g.drawImage(map, SCREENOFFSETX + x, screenOffsetY + y); } +*/ function setBlockTilesAsBackground() { set_bkg_data(BLOCKTILES); @@ -1056,8 +1059,8 @@ function generateLevel() "RAM"; if(USECCODE) { - var cellstack = new Uint8Array(MAXBOARDSIZE + 1); - var neighbours = new Uint8Array(4); + //var cellstack = new Uint8Array(MAXBOARDSIZE + 1); + //var neighbours = new Uint8Array(4); var addrLevel = E.getAddressOf(level,true); c.generateLevel(addrLevel, boardWidth, boardHeight); } @@ -1621,6 +1624,7 @@ function levelSelect() { // -------------------------------------------------------------------------------------------------- // printing functions // -------------------------------------------------------------------------------------------------- +/* function setCharAt(str, index, chr) { "RAM"; if (index > str.length - 1) return str; @@ -1676,6 +1680,7 @@ function printNumber(ax, ay, aNumber, maxDigits, noScreenOffset) { set_bkg_tile_xy(ax + (maxDigits - ret.digits) + c, ay, ret.string.charCodeAt(buffSize - ret.digits + c) + 32, noScreenOffset); } } +*/ //print a message on the title screen on ax,ay, the tileset from titlescreen contains an alphabet function printMessage(ax, ay, amsg, noScreenOffset) { diff --git a/apps/waternet/metadata.json b/apps/waternet/metadata.json index 59170617a..0f7703d57 100644 --- a/apps/waternet/metadata.json +++ b/apps/waternet/metadata.json @@ -1,7 +1,7 @@ { "id": "waternet", "name": "Waternet", "shortName":"Waternet", - "version":"0.02", + "version": "0.03", "description": "Puzzle game where water needs to flow through pipes by sliding or rotating them", "icon": "app.png", "screenshots": [{"url":"screenshot2.png"},{"url":"screenshot1.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"},{"url":"screenshot5.png"},{"url":"screenshot6.png"}], diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog index 8c8a323de..76cc04977 100644 --- a/apps/waypointer/ChangeLog +++ b/apps/waypointer/ChangeLog @@ -7,3 +7,5 @@ 0.07: Add settings file with the option to disable the slow direction updates 0.08: Use tilt compensation from new magnav library 0.09: Convert Yes/No On/Off in settings to checkboxes +0.10: Minor code improvements +0.11: Minor code improvements diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js index 40e4cf974..ff2412730 100644 --- a/apps/waypointer/app.js +++ b/apps/waypointer/app.js @@ -112,8 +112,8 @@ function read_compass() { /***** END Compass ***********/ -var speed = 0; -var satellites = 0; +//var speed = 0; +//var satellites = 0; var wp; var dist=0; @@ -190,9 +190,9 @@ var savedfix; function onGPS(fix) { savedfix = fix; - if (fix!==undefined){ + /*if (fix!==undefined){ satellites = fix.satellites; - } + }*/ if (candraw) { if (fix!==undefined && fix.fix==1){ @@ -209,13 +209,13 @@ var intervalRef; function stopdraw() { candraw=false; - prev_course = -1; + //prev_course = -1; if(intervalRef) {clearInterval(intervalRef);} } function startTimers() { candraw=true; - intervalRefSec = setInterval(function() { + /*intervalRefSec =*/ setInterval(function() { read_compass(); }, 500); } diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json index 189639d61..a997822fa 100644 --- a/apps/waypointer/metadata.json +++ b/apps/waypointer/metadata.json @@ -1,7 +1,7 @@ { "id": "waypointer", "name": "Way Pointer", - "version": "0.09", + "version": "0.11", "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", "icon": "waypointer.png", "tags": "tool,outdoors,gps", diff --git a/apps/waypoints/ChangeLog b/apps/waypoints/ChangeLog index ed3d1bc9b..f8863ebaf 100644 --- a/apps/waypoints/ChangeLog +++ b/apps/waypoints/ChangeLog @@ -2,3 +2,4 @@ 0.02: Merge waypoint_editor here, so waypoints can be edited on device, too. 0.03: Do not register as type waypoint - show in launcher Fixes for Bangle.js 1 & not installed textinput +0.04: Minor code improvements diff --git a/apps/waypoints/metadata.json b/apps/waypoints/metadata.json index b2121e00d..bc88cff2e 100644 --- a/apps/waypoints/metadata.json +++ b/apps/waypoints/metadata.json @@ -1,6 +1,6 @@ { "id": "waypoints", "name": "Waypoints", - "version":"0.03", + "version": "0.04", "description": "Provides 'waypoints.json' used by various navigation apps, as well as a way to edit it from the App Loader or from the device", "icon": "app.png", "tags": "tool,outdoors,gps", diff --git a/apps/waypoints/waypoints.app.js b/apps/waypoints/waypoints.app.js index fec1af993..0c63e9183 100644 --- a/apps/waypoints/waypoints.app.js +++ b/apps/waypoints/waypoints.app.js @@ -3,9 +3,6 @@ var Layout = require("Layout"); const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2 -const W = g.getWidth(); -const H = g.getHeight(); - var wp = require('Storage').readJSON("waypoints.json", true) || []; // Use this with corrupted waypoints //var wp = []; @@ -50,7 +47,7 @@ function mainMenu() { } function updateGps() { - let have = false, lat = "lat", lon = "lon", alt = "alt", speed = "speed"; + let lat = "lat", lon = "lon", alt = "alt", speed = "speed"; if (cancel_gps) return; @@ -63,7 +60,6 @@ function updateGps() { lon = "" + lon(fix.lon); alt = "alt " + fix.alt.toFixed(0) + "m"; speed = "speed " + fix.speed.toFixed(1) + "kt"; - have = true; } g.reset().setFont("Vector", 20) @@ -266,7 +262,7 @@ function ask01(t, cb) { } function askCoordinate(t1, t2, callback) { - let sign = 1; + //let sign = 1; ask01(t1, function(sign) { switch (mode) { case 0: s = "DDD.dddd"; break; @@ -297,7 +293,6 @@ function askCoordinate(t1, t2, callback) { } function askPosition(callback) { - let full = ""; askCoordinate("NS", "0", function(lat) { askCoordinate("EW", "", function(lon) { callback(lat, lon); diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog index 5a6db5cb7..8f13618aa 100644 --- a/apps/widChargingStatus/ChangeLog +++ b/apps/widChargingStatus/ChangeLog @@ -1,2 +1,4 @@ 0.01: First release. 0.02: No functional changes, just moved codebase to Typescript. +0.03: Also buzz on disconnect from charging +0.04: Minor code improvements diff --git a/apps/widChargingStatus/metadata.json b/apps/widChargingStatus/metadata.json index 63a59fe41..0feca7b44 100644 --- a/apps/widChargingStatus/metadata.json +++ b/apps/widChargingStatus/metadata.json @@ -2,7 +2,7 @@ "name": "Charging Status", "shortName":"ChargingStatus", "icon": "widget.png", - "version":"0.02", + "version": "0.04", "type": "widget", "description": "A simple widget that shows a yellow lightning icon to indicate whenever the watch is charging. This way one can see the charging status at a glance, no matter which battery widget is being used.", "tags": "widget", diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js index 628fac043..873383cf8 100644 --- a/apps/widChargingStatus/widget.js +++ b/apps/widChargingStatus/widget.js @@ -10,19 +10,20 @@ }); } } - WIDGETS.chargingStatus = { + WIDGETS["chargingStatus"] = { area: 'tr', width: Bangle.isCharging() ? iconWidth : 0, draw: draw, }; Bangle.on('charging', function (charging) { - var widget = WIDGETS.chargingStatus; + var widget = WIDGETS["chargingStatus"]; if (widget) { if (charging) { Bangle.buzz(); widget.width = iconWidth; } else { + Promise.resolve().then(function () { return require("buzz").pattern("..;"); }); widget.width = 0; } Bangle.drawWidgets(); diff --git a/apps/widChargingStatus/widget.ts b/apps/widChargingStatus/widget.ts index a161d5408..db8f9a490 100644 --- a/apps/widChargingStatus/widget.ts +++ b/apps/widChargingStatus/widget.ts @@ -16,21 +16,20 @@ } } - // @ts-ignore - WIDGETS.chargingStatus = { + WIDGETS["chargingStatus"] = { area: 'tr', width: Bangle.isCharging() ? iconWidth : 0, draw: draw, }; Bangle.on('charging', (charging) => { - // @ts-ignore - const widget = WIDGETS.chargingStatus; + const widget = WIDGETS["chargingStatus"]; if (widget) { if (charging) { Bangle.buzz(); widget.width = iconWidth; } else { + Promise.resolve().then(() => require("buzz").pattern("..;")); widget.width = 0; } Bangle.drawWidgets(); // re-layout widgets diff --git a/apps/widalarm/ChangeLog b/apps/widalarm/ChangeLog index 63568a9bd..a26c63b9f 100644 --- a/apps/widalarm/ChangeLog +++ b/apps/widalarm/ChangeLog @@ -1 +1,2 @@ 0.01: Moved out of 'alarm' app +0.02: Decouple reloading of widget when alarms change diff --git a/apps/widalarm/metadata.json b/apps/widalarm/metadata.json index b91457138..e3cb2d0c1 100644 --- a/apps/widalarm/metadata.json +++ b/apps/widalarm/metadata.json @@ -1,7 +1,7 @@ { "id": "widalarm", "name": "Alarms Widget", - "version": "0.01", + "version": "0.02", "description": "Displays an alarm icon in the widgets bar if any alarm is active", "icon": "app.png", "type": "widget", diff --git a/apps/widalarm/widget.js b/apps/widalarm/widget.js index 964176fc7..15fb0b9a3 100644 --- a/apps/widalarm/widget.js +++ b/apps/widalarm/widget.js @@ -6,3 +6,9 @@ WIDGETS["alarm"]={area:"tl",width:0,draw:function() { } }; WIDGETS["alarm"].reload(); +Bangle.on("alarmReload", () => { + if (WIDGETS["alarm"]) { + WIDGETS["alarm"].reload(); + Bangle.drawWidgets(); + } +}); diff --git a/apps/widalarmeta/ChangeLog b/apps/widalarmeta/ChangeLog index 1ef003586..3c46a77ba 100644 --- a/apps/widalarmeta/ChangeLog +++ b/apps/widalarmeta/ChangeLog @@ -12,3 +12,4 @@ 0.08: Selectable font. Allow to disable hour padding. 0.09: Match draw() API e.g. to allow wid_edit to alter this widget 0.10: Change 4x5 font to 6x8, teletext is now default font +0.11: Bugfix: handle changes in alarms (e.g. done without a load, such as via fastload) diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json index b23c2039c..d43da013c 100644 --- a/apps/widalarmeta/metadata.json +++ b/apps/widalarmeta/metadata.json @@ -2,7 +2,7 @@ "id": "widalarmeta", "name": "Alarm & Timer ETA", "shortName": "Alarm ETA", - "version": "0.10", + "version": "0.11", "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).", "icon": "widget.png", "type": "widget", diff --git a/apps/widalarmeta/widget.js b/apps/widalarmeta/widget.js index c7542ada5..8d96bca81 100644 --- a/apps/widalarmeta/widget.js +++ b/apps/widalarmeta/widget.js @@ -19,7 +19,11 @@ loadSettings(); function getNextAlarm(date) { - const alarms = (require("Storage").readJSON("sched.json",1) || []).filter(alarm => alarm.on && alarm.hidden !== true); + const alarms = require("sched") + .getAlarms() + // more precise filtering is done using getTimeToAlarm() below + .filter(alarm => alarm.on && alarm.hidden !== true); + WIDGETS["widalarmeta"].numActiveAlarms = alarms.length; if (alarms.length > 0) { const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY); @@ -116,16 +120,18 @@ } /* draw */ if (config.maxhours > 0) { - // add your widget WIDGETS["widalarmeta"]={ area:"tl", width: 0, // hide by default = assume no timer draw:draw, - reload: () => { + reload: function () { + this.nextAlarm = undefined; + loadSettings(); - g.clear(); Bangle.drawWidgets(); }, }; + + Bangle.on("alarmReload", () => WIDGETS["widalarmeta"].reload()); } })(); diff --git a/apps/widalt/ChangeLog b/apps/widalt/ChangeLog index ec66c5568..b7e50d38d 100644 --- a/apps/widalt/ChangeLog +++ b/apps/widalt/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Minor code improvements diff --git a/apps/widalt/metadata.json b/apps/widalt/metadata.json index 309c5bf2c..31af738f7 100644 --- a/apps/widalt/metadata.json +++ b/apps/widalt/metadata.json @@ -2,7 +2,7 @@ { "id": "widalt", "name": "Altimeter widget", - "version": "0.01", + "version": "0.02", "description": "Displays barometric altitude", "readme": "README.md", "icon": "widalt.png", diff --git a/apps/widalt/widalt.settings.js b/apps/widalt/widalt.settings.js index 57993474e..1178c6fcf 100644 --- a/apps/widalt/widalt.settings.js +++ b/apps/widalt/widalt.settings.js @@ -2,7 +2,7 @@ var settings = Object.assign({ interval: 5000, }, require('Storage').readJSON("widalt.json", true) || {}); - o=Bangle.getOptions(); + const o=Bangle.getOptions(); Bangle.getPressure().then((p)=>{ E.showMenu({ "" : { "title" : "Altimeter Widget" }, diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index 5b30916ce..e703a7dfb 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -11,3 +11,4 @@ 0.08: Compatibility with hideable Widgets 0.09: Do not immediately measure on start, keep interval Add plot history to settings +0.10: Minor code improvements diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index ad2320cd5..89f6e3a4d 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,7 +2,7 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.09", + "version": "0.10", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index 2947b60f3..c7532f443 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -219,7 +219,7 @@ function barometerPressureHandler(e) { const pressure = e.pressure; if (isValidPressureValue(pressure)) { currentPressures.unshift(pressure); - median = currentPressures.slice().sort(); + let median = currentPressures.slice().sort(); if (median.length > 10) { var mid = median.length >> 1; diff --git a/apps/widbata/widbata.wid.js b/apps/widbata/widbata.wid.js index 16554057f..4ab6d76f1 100644 --- a/apps/widbata/widbata.wid.js +++ b/apps/widbata/widbata.wid.js @@ -5,7 +5,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() { var s = 26; var t = 13; // thickness - var x = this.x, y = this.y + 5; + var x = this.x, y = this.y + 3; g.reset(); g.setColor(g.theme.fg); g.fillRect(x,y+2,x+s-4,y+2+t); // outer diff --git a/apps/widbattpwr/ChangeLog b/apps/widbattpwr/ChangeLog new file mode 100644 index 000000000..929eb99e9 --- /dev/null +++ b/apps/widbattpwr/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial fork from hwid_a_battery_widget +0.02: Show battery percentage (instead of power) if charging +0.03: Use `power_usage` module diff --git a/apps/widbattpwr/README.md b/apps/widbattpwr/README.md new file mode 100644 index 000000000..22a575166 --- /dev/null +++ b/apps/widbattpwr/README.md @@ -0,0 +1,15 @@ +# Battery Power Widget + +Show the time remaining at the current power consumption, and battery percentage via shading of the text and a percentage bar. + +Battery percentage can be seen: +- Temporarily by tapping the widget +- By charging the watch + +Requires firmware 2v23 or above. + +This is a copy of `hwid_a_battery_widget` (that being a copy of `wid_a_battery_widget`). + +## Creator + +[@bobrippling](https://github.com/bobrippling) diff --git a/apps/widbattpwr/metadata.json b/apps/widbattpwr/metadata.json new file mode 100644 index 000000000..2a41169ab --- /dev/null +++ b/apps/widbattpwr/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbattpwr", + "name": "Battery power and percentage widget", + "shortName": "Batt Pwr", + "icon": "widget.png", + "version": "0.03", + "type": "widget", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "description": "A battery widget showing percentage (via shading) and time remaining at current power consumption", + "tags": "widget,battery", + "provides_widgets": ["battery"], + "storage": [ + { + "name": "widbattpwr.wid.js", + "url": "widget.js" + } + ] +} diff --git a/apps/widbattpwr/widget.js b/apps/widbattpwr/widget.js new file mode 100644 index 000000000..83a4917f5 --- /dev/null +++ b/apps/widbattpwr/widget.js @@ -0,0 +1,74 @@ +(function () { + var intervalLow = 60000; + var intervalHigh = 2000; + var width = 30; + var height = 24; + var showPct = false; + var powerColour = function (pwr) { + return pwr >= 23000 + ? "#f00" + : pwr > 2000 + ? "#fc0" + : "#0f0"; + }; + var drawBar = function (x, y, batt) { + return g.fillRect(x + 1, y + height - 3, x + 1 + (width - 2) * batt / 100, y + height - 1); + }; + var drawString = function (x, y, txt) { + return g.drawString(txt, x + 14, y + 10); + }; + function draw() { + var x = this.x; + var y = this.y; + var _a = require("power_usage").get(), usage = _a.usage, hrsLeft = _a.hrsLeft, batt = _a.batt; + var pwrColour = powerColour(usage); + g.reset() + .setBgColor(g.theme.bg) + .clearRect(x, y, x + width - 1, y + height - 1); + g.setColor(g.theme.fg); + drawBar(x, y, 100); + g.setColor(pwrColour); + drawBar(x, y, batt); + g.setFontAlign(0, 0); + g.setFont("Vector", 16); + { + var txt = void 0; + if (showPct || Bangle.isCharging()) { + txt = "".concat(batt, "%"); + } + else { + var days = hrsLeft / 24; + txt = days >= 1 ? "".concat(Math.round(Math.min(days, 99)), "d") : "".concat(Math.round(hrsLeft), "h"); + } + var txth = 14; + g.setColor(g.theme.fg); + g.setClipRect(x, y, x + width, y + txth); + drawString(x, y, txt); + g.setColor(pwrColour); + g.setClipRect(x, y + txth * (1 - batt / 100), x + width, y + txth); + drawString(x, y, txt); + } + } + var id = setInterval(function () { + var w = WIDGETS["battpwr"]; + w.draw(w); + }, intervalLow); + Bangle.on("charging", function (charging) { + changeInterval(id, charging ? intervalHigh : intervalLow); + }); + Bangle.on("touch", function (_btn, xy) { + if (WIDGETS["back"] || !xy) + return; + var oversize = 5; + var w = WIDGETS["battpwr"]; + var x = xy.x, y = xy.y; + if (w.x - oversize <= x && x < w.x + width + oversize + && w.y - oversize <= y && y < w.y + height + oversize) { + E.stopEventPropagation && E.stopEventPropagation(); + showPct = true; + setTimeout(function () { return (showPct = false, w.draw(w)); }, 1000); + w.draw(w); + } + }); + WIDGETS["battpwr"] = { area: "tr", width: width, draw: draw }; +})(); diff --git a/apps/widbattpwr/widget.png b/apps/widbattpwr/widget.png new file mode 100644 index 000000000..e5a8284a1 Binary files /dev/null and b/apps/widbattpwr/widget.png differ diff --git a/apps/widbattpwr/widget.ts b/apps/widbattpwr/widget.ts new file mode 100644 index 000000000..2b92398d2 --- /dev/null +++ b/apps/widbattpwr/widget.ts @@ -0,0 +1,88 @@ +(() => { + const intervalLow = 60000; + const intervalHigh = 2000; + const width = 30; + const height = 24; + let showPct = false; + + const powerColour = (pwr: number) => + pwr >= 23000 + ? "#f00" // red, e.g. GPS ~20k + : pwr > 2000 + ? "#fc0" // yellow, e.g. CPU ~1k, HRM ~700 + : "#0f0"; // green: ok + + const drawBar = (x: number, y: number, batt: number) => + g.fillRect(x+1, y+height-3, x+1+(width-2)*batt/100, y+height-1); + + const drawString = (x: number, y: number, txt: string) => + g.drawString(txt, x + 14, y + 10); + + function draw(this: Widget) { + let x = this.x!; + let y = this.y!; + + const { usage, hrsLeft, batt } = require("power_usage").get(); + const pwrColour = powerColour(usage); + + g.reset() + .setBgColor(g.theme.bg) + .clearRect(x, y, x + width - 1, y + height - 1); + + g.setColor(g.theme.fg); + drawBar(x, y, 100); + g.setColor(pwrColour); + drawBar(x, y, batt); + + g.setFontAlign(0, 0); + g.setFont("Vector", 16); + { + let txt; + if(showPct || Bangle.isCharging()){ + txt = `${batt}%`; + }else{ + const days = hrsLeft / 24; + txt = days >= 1 ? `${Math.round(Math.min(days, 99))}d` : `${Math.round(hrsLeft)}h`; + } + + // draw time remaining, then shade it based on batt % + const txth = 14; + g.setColor(g.theme.fg); + g.setClipRect(x, y, x + width, y + txth); + drawString(x, y, txt); + + g.setColor(pwrColour); + g.setClipRect(x, y + txth * (1 - batt / 100), x + width, y + txth); + drawString(x, y, txt); + } + } + + const id = setInterval(() => { + const w = WIDGETS["battpwr"]!; + w.draw(w); + }, intervalLow); + + Bangle.on("charging", charging => { + changeInterval(id, charging ? intervalHigh : intervalLow); + }); + + Bangle.on("touch", (_btn, xy) => { + if(WIDGETS["back"] || !xy) return; + + const oversize = 5; + const w = WIDGETS["battpwr"]!; + const { x, y } = xy; + + if(w.x! - oversize <= x && x < w.x! + width + oversize + && w.y! - oversize <= y && y < w.y! + height + oversize) + { + E.stopEventPropagation && E.stopEventPropagation(); + + showPct = true; + setTimeout(() => (showPct = false, w.draw(w)), 1000); + w.draw(w); + } + }); + + WIDGETS["battpwr"] = { area: "tr", width, draw }; +})(); diff --git a/apps/widbgjs/ChangeLog b/apps/widbgjs/ChangeLog index a88c5f063..5d92df43f 100644 --- a/apps/widbgjs/ChangeLog +++ b/apps/widbgjs/ChangeLog @@ -1,2 +1,4 @@ 0.01: First release 0.02: Fixed settings changes are actually reflected now and old values are strikethrough +0.03: Minor code improvements +0.04: Fix after regression in 0.03 \ No newline at end of file diff --git a/apps/widbgjs/metadata.json b/apps/widbgjs/metadata.json index fce68514c..4b37dd9e3 100644 --- a/apps/widbgjs/metadata.json +++ b/apps/widbgjs/metadata.json @@ -4,7 +4,7 @@ "shortName":"BG Widget", "icon": "screenshot.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.02", + "version": "0.04", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", diff --git a/apps/widbgjs/widget.js b/apps/widbgjs/widget.js index 4b81c4885..c95ecd032 100644 --- a/apps/widbgjs/widget.js +++ b/apps/widbgjs/widget.js @@ -66,10 +66,12 @@ } // turn the arrow thanks to (https://forum.espruino.com/conversations/344607/) const p180 = Math.PI / 180; + /* // a is defined above var r = 21; var x = r * Math.sin(a * p180); var y = r * Math.cos(a * p180); + */ return a * p180; } @@ -114,7 +116,7 @@ console.log(settings.unitIsMmol.toString()); loadVals(); - outpt = getBG(storedData.bg); + let outpt = getBG(storedData.bg); if (outpt === null) { // this means no value has been received yet outpt = "BG"; diff --git a/apps/widbtstates/widget.js b/apps/widbtstates/widget.js index e80da4082..105e4111d 100644 --- a/apps/widbtstates/widget.js +++ b/apps/widbtstates/widget.js @@ -17,12 +17,12 @@ }; var colours = (_a = {}, _a[1] = { - false: "#fff", + false: "#000", true: "#fff", }, _a[2] = { - false: "#0ff", - true: "#00f", + false: "#00f", + true: "#0ff", }, _a); WIDGETS["bluetooth"] = { diff --git a/apps/widbtstates/widget.ts b/apps/widbtstates/widget.ts index 8f02c1b8c..40f50f627 100644 --- a/apps/widbtstates/widget.ts +++ b/apps/widbtstates/widget.ts @@ -30,12 +30,12 @@ } } = { [State.Active]: { - false: "#fff", + false: "#000", true: "#fff", }, [State.Connected]: { - false: "#0ff", - true: "#00f", + false: "#00f", + true: "#0ff", }, }; diff --git a/apps/widcw/ChangeLog b/apps/widcw/ChangeLog index 07b8f7424..4f06d0ff6 100644 --- a/apps/widcw/ChangeLog +++ b/apps/widcw/ChangeLog @@ -1,2 +1,3 @@ 0.01: First version -0.02: Fix memory leak \ No newline at end of file +0.02: Fix memory leak +0.03: Minor code improvements diff --git a/apps/widcw/metadata.json b/apps/widcw/metadata.json index 467ab1729..b3fa5898c 100644 --- a/apps/widcw/metadata.json +++ b/apps/widcw/metadata.json @@ -1,7 +1,7 @@ { "id": "widcw", "name": "Calendar Week Widget", - "version": "0.02", + "version": "0.03", "description": "Widget which shows the current calendar week", "icon": "widget.png", "type": "widget", diff --git a/apps/widcw/widget.js b/apps/widcw/widget.js index e33ad0aad..bce6a1e92 100644 --- a/apps/widcw/widget.js +++ b/apps/widcw/widget.js @@ -7,7 +7,7 @@ var date = new Date(); // Calculate calendar week (https://stackoverflow.com/a/6117889) - getCW= function(date){ + const getCW = function(date){ var d=new Date(date.getFullYear(), date.getMonth(), date.getDate()); var dayNum = d.getDay() || 7; d.setDate(d.getDate() + 4 - dayNum); diff --git a/apps/widdevst/ChangeLog b/apps/widdevst/ChangeLog index cee46ac6a..e5caadb84 100644 --- a/apps/widdevst/ChangeLog +++ b/apps/widdevst/ChangeLog @@ -1,3 +1,5 @@ 0.01: First version 0.02: Support for Bangle.js 2 0.03: Update storage usage and perform GC every minute +0.04: Add ability to only redraw when a peripheral state changes +0.05: Adjust default (of previous change) to preserve original behaviour diff --git a/apps/widdevst/README.md b/apps/widdevst/README.md index 49affc78d..2ac3c8388 100644 --- a/apps/widdevst/README.md +++ b/apps/widdevst/README.md @@ -13,3 +13,7 @@ at fixed positions, and two bars - bottom to top: usage of RAM in green if below 50%, orange if between 50% and 80%, and red if above 80%. + +The widget will redraw more frequently when unlocked. + +It can be configured to avoid redrawing if all monitored peripherals are off, waiting until it hears from them (meaning you won't see regular RAM/Storage updates, but save battery by avoiding drawing). This can be configured by writing `{"redrawBars":0}` to `widdevst.settings.json`. diff --git a/apps/widdevst/metadata.json b/apps/widdevst/metadata.json index f2be2bec8..d9d39d005 100644 --- a/apps/widdevst/metadata.json +++ b/apps/widdevst/metadata.json @@ -1,6 +1,6 @@ { "id": "widdevst", "name": "Device Status Widget", - "version": "0.03", + "version": "0.05", "description": "Shows power status of Bluetooth, Compass, GPS and Heart Rate Monitor as well as storage and memory usage.", "icon": "icon.png", "type": "widget", @@ -9,5 +9,8 @@ "readme": "README.md", "storage": [ {"name": "widdevst.wid.js", "url": "wid.js"} + ], + "data":[ + {"name": "widdevst.settings.json"} ] } diff --git a/apps/widdevst/wid.js b/apps/widdevst/wid.js index 3cd5758ab..1d1e8e2fc 100644 --- a/apps/widdevst/wid.js +++ b/apps/widdevst/wid.js @@ -1,9 +1,13 @@ (() => { var stat = {date: 0}; + var d = Date.now(); + var settings = require("Storage").readJSON("widdevst.settings.json", 1)||{}; + var redrawBars = "redrawBars" in settings ? settings.redrawBars : true; + delete settings; WIDGETS.devst = {area: "tr", width: 22, draw: function() { + d = Date.now(); if (WIDGETS.devst._draw) return; - var d = new Date(); var t; if ((d - stat.date) < 6e4) { t = process.memory(false); @@ -20,12 +24,15 @@ g.clearRect(x, y, x + 21, y + 23); g.drawRect(x + 2, y + 1, x + 20, y + 21); g.setFont('6x8', 1); - if (NRF.getSecurityStatus().connected) g.drawString('B', x + 5, y + 3); - if (Bangle.isCompassOn()) g.drawString('C', x + 13, y + 3); - if (Bangle.isGPSOn()) g.drawString('G', x + 5, y + 12); - if (Bangle.isHRMOn()) g.drawString('H', x + 13, y + 12); + var again = false; + if (NRF.getSecurityStatus().connected) g.drawString('B', x + 5, y + 3), again = true; + if (Bangle.isCompassOn()) g.drawString('C', x + 13, y + 3), again = true; + if (Bangle.isGPSOn()) g.drawString('G', x + 5, y + 12), again = true; + if (Bangle.isHRMOn()) g.drawString('H', x + 13, y + 12), again = true; g.setColor(col(stat.sto)); g.drawRect(x + 2, y + 21, x + 2 + stat.sto * 18, y + 22); g.setColor(col(t)); g.drawRect(x + 1, y + 21 - t * 20, x + 2, y + 21); + // if there's nothing active, don't queue a redraw (rely on Bangle.on(...) below) + if (redrawBars || again) setTimeout(draw, drawTime()); }}; function col(p) { @@ -33,18 +40,13 @@ } var draw = WIDGETS.devst.draw.bind(WIDGETS.devst); - var iid = setInterval(draw, Bangle.isLocked() ? 6e4 : 2e3); - Bangle.on('lcdPower', (on) => { - if (on) { - draw(); - if (!iid) iid = setInterval(draw, Bangle.isLocked() ? 6e4 : 2e3); - } else if (iid) iid = clearInterval(iid); - }); - Bangle.on('lock', (on) => { - if (iid) { - clearInterval(iid); - iid = setInterval(draw, on ? 6e4 : 2e3); - } - }); + var drawTime = () => Bangle.isLocked() ? 6e4 : 2e3; + var throttledDraw = () => Date.now() - d > drawTime() && draw(); + + Bangle.on("HRM", throttledDraw); + Bangle.on("GPS", throttledDraw); + Bangle.on("mag", throttledDraw); + NRF.on("connect", throttledDraw); + draw(); })(); diff --git a/apps/widdst/ChangeLog b/apps/widdst/ChangeLog index d1ad50fe2..cedeaa5b4 100644 --- a/apps/widdst/ChangeLog +++ b/apps/widdst/ChangeLog @@ -2,4 +2,5 @@ 0.02: Checks for correct firmware; E.setDST(...) moved to boot.js 0.03: Convert Yes/No On/Off in settings to checkboxes 0.04: Give the boot file the highest priority to ensure it runs before sched (fix #2663) -0.05: Tweaks to ensure Gadgetbridge can't overwrite timezone on 2v19.106 and later \ No newline at end of file +0.05: Tweaks to ensure Gadgetbridge can't overwrite timezone on 2v19.106 and later +0.06: If fastload is present, ensure DST changes are still applied when leaving settings diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json index 006e03416..495f06086 100644 --- a/apps/widdst/metadata.json +++ b/apps/widdst/metadata.json @@ -1,6 +1,6 @@ { "id": "widdst", "name": "Daylight Saving", - "version":"0.05", + "version":"0.06", "description": "Widget to set daylight saving rules. Requires Espruino 2v15 or later - see the instructions below for more information.", "icon": "icon.png", "type": "widget", diff --git a/apps/widdst/settings.js b/apps/widdst/settings.js index 7363aa6bf..0017cc499 100644 --- a/apps/widdst/settings.js +++ b/apps/widdst/settings.js @@ -33,8 +33,11 @@ at: 0 }; + var writtenSettings = false; + function writeSettings() { require('Storage').writeJSON("widdst.json", settings); + writtenSettings = true; } function writeSubMenuSettings() { @@ -136,7 +139,15 @@ "": { "Title": /*LANG*/"Daylight Saving" }, - "< Back": () => back(), + "< Back": () => { + if(writtenSettings && global._load){ + // disable fastload to ensure settings are applied + // when we exit the settings app + global.load = global._load; + delete global._load; + } + back(); + }, /*LANG*/"Enabled": { value: !!settings.has_dst, onchange: v => { diff --git a/apps/widhid/wid.ts b/apps/widhid/wid.ts index 1de293b36..c92512959 100644 --- a/apps/widhid/wid.ts +++ b/apps/widhid/wid.ts @@ -4,7 +4,7 @@ console.log("widhid: can't enable, HID setting isn't \"kbmedia\""); return; } - // @ts-ignore + // @ts-expect-error espruino-specific delete delete settings; let anchor = {x:0,y:0}; @@ -128,7 +128,7 @@ if(connected) Bangle.on("swipe", onSwipe); - // @ts-ignore + // @ts-expect-error espruino-specific delete delete connected; NRF.on("connect", () => { diff --git a/apps/widhwbttm/ChangeLog b/apps/widhwbttm/ChangeLog index 7d3aafc41..4e8b54a18 100644 --- a/apps/widhwbttm/ChangeLog +++ b/apps/widhwbttm/ChangeLog @@ -1,3 +1,5 @@ 0.01: 1st ver, inspired in some code from widclkbttm (Digital clock bttom widget) 0.02: Correction, intervals, dynamic color and font size depending on device 0.03: minor corrections, and color depending on theme +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/widhwbttm/metadata.json b/apps/widhwbttm/metadata.json index 8a6957a46..b9b99db42 100644 --- a/apps/widhwbttm/metadata.json +++ b/apps/widhwbttm/metadata.json @@ -2,7 +2,7 @@ "id": "widhwbttm", "name": "HW stats (Bottom) widget", "shortName": "Digital clock Bottom Widget", - "version": "0.03", + "version": "0.05", "description": "Displays technical info, such as model, ver, temperatura or mem stats in the bottom of the screen (may not be compatible with some apps)", "icon": "widhwbttm.png", "type": "widget", diff --git a/apps/widhwbttm/widhwbttm.wid.js b/apps/widhwbttm/widhwbttm.wid.js index 551e2005b..72cdd6cfe 100644 --- a/apps/widhwbttm/widhwbttm.wid.js +++ b/apps/widhwbttm/widhwbttm.wid.js @@ -1,5 +1,5 @@ (function() { - let intervalRef = null; + //let intervalRef = null; var v_count; // show stats var v_str_hw=new String(); //if (process.env.BOARD=='BANGLEJS'||process.env.BOARD=='EMSCRIPTEN') var v_bfont_size=2; @@ -26,7 +26,7 @@ } else { // text prefix has to be 4char - stor=require("Storage").getStats(); + const stor=require("Storage").getStats(); if (v_count==4) { v_str_hw="Fre "+process.memory().free; //+"/"+process.memory().total; @@ -55,5 +55,5 @@ } //end draw WIDGETS["wdhwbttm"]={area:"bl",width:100,draw:draw}; -if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdhwbttm"].draw(), 10*1000); +if (Bangle.isLCDOn) /*intervalRef =*/ setInterval(()=>WIDGETS["wdhwbttm"].draw(), 10*1000); })() diff --git a/apps/widlockunlock/ChangeLog b/apps/widlockunlock/ChangeLog index b5efcaa86..40a842cc7 100644 --- a/apps/widlockunlock/ChangeLog +++ b/apps/widlockunlock/ChangeLog @@ -1,2 +1,3 @@ 0.01: First commit 0.02: Add tap-to-lock functionality +0.03: Disable tap-to-lock if back button is present diff --git a/apps/widlockunlock/metadata.json b/apps/widlockunlock/metadata.json index cc4fa76cd..c3f871c84 100644 --- a/apps/widlockunlock/metadata.json +++ b/apps/widlockunlock/metadata.json @@ -1,8 +1,8 @@ { "id": "widlockunlock", "name": "Lock/Unlock Widget", - "version": "0.02", - "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise. Tap to lock the lcd", + "version": "0.03", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise. Tap to lock the lcd (unless the back button is shown, in which case it takes priority)", "icon": "widget.png", "type": "widget", "tags": "widget,lock", diff --git a/apps/widlockunlock/widget.js b/apps/widlockunlock/widget.js index cfbbc87a3..4b78b440c 100644 --- a/apps/widlockunlock/widget.js +++ b/apps/widlockunlock/widget.js @@ -1,6 +1,8 @@ Bangle.on("lock", () => Bangle.drawWidgets()); Bangle.on('touch', (_btn, xy) => { + if (WIDGETS["back"]) return; + const oversize = 5; const w = WIDGETS.lockunlock; @@ -11,6 +13,8 @@ Bangle.on('touch', (_btn, xy) => { if(w.x - oversize <= x && x < w.x + 14 + oversize && w.y - oversize <= y && y < w.y + 24 + oversize) { + E.stopEventPropagation && E.stopEventPropagation(); + Bangle.setLocked(true); const backlightTimeout = Bangle.getOptions().backlightTimeout; // ms diff --git a/apps/widmessages/ChangeLog b/apps/widmessages/ChangeLog index 507d9c13b..314b1490c 100644 --- a/apps/widmessages/ChangeLog +++ b/apps/widmessages/ChangeLog @@ -4,3 +4,4 @@ 0.03: Fix messages not showing if UI auto-open is disabled 0.04: Now shows message icons again (#2416) 0.05: Match draw() API e.g. to allow wid_edit to alter this widget +0.06: Fix bug that meant that only one widget was shown (now 3 unless changed in Settings->Apps->Messages->Widget messages) \ No newline at end of file diff --git a/apps/widmessages/metadata.json b/apps/widmessages/metadata.json index f058beacc..436b77d3e 100644 --- a/apps/widmessages/metadata.json +++ b/apps/widmessages/metadata.json @@ -1,7 +1,7 @@ { "id": "widmessages", "name": "Message Widget", - "version": "0.05", + "version": "0.06", "description": "Widget showing new messages", "icon": "app.png", "type": "widget", diff --git a/apps/widmessages/widget.js b/apps/widmessages/widget.js index 357ca06e3..0351fbead 100644 --- a/apps/widmessages/widget.js +++ b/apps/widmessages/widget.js @@ -1,13 +1,5 @@ -(() => { - if ((require("Storage").readJSON("messages.settings.json", true) || {}).maxMessages===0) return; - - function filterMessages(msgs) { - return msgs.filter(msg => msg.new && msg.id != "music") - .map(m => m.src) // we only need this for icon/color - .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); - } - - // NOTE when adding a custom "essages" widget: +if ((require("Storage").readJSON("messages.settings.json", true) || {}).maxMessages!==0) { + // NOTE when adding a custom "messages" widget: // the name still needs to be "messages": the library calls WIDGETS["messages'].hide()/show() // see e.g. widmsggrid WIDGETS["messages"] = { @@ -48,6 +40,9 @@ if (this.hidden) return; if (type==="music") return; if (msg.id && !msg.new && msg.t!=="remove") return; + let filterMessages = msgs => msgs.filter(msg => msg.new && msg.id != "music") + .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i) // only include one of each type + .map(m => m.src); // we only need this for icon/color; this.srcs = filterMessages(require("messages").getMessages(msg)); const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); this.width = 24 * E.clip(this.srcs.length, 0, settings.maxMessages); @@ -66,11 +61,11 @@ Bangle.drawWidgets(); } }, show() { - delete this.hidden + delete this.hidden; this.onMsg("show", {}); // reload messages+redraw } }; Bangle.on("message", WIDGETS["messages"].onMsg.bind(WIDGETS["messages"])); WIDGETS["messages"].onMsg("init", {}); // abuse type="init" to prevent Bangle.drawWidgets(); -})(); +} \ No newline at end of file diff --git a/apps/widmnth/ChangeLog b/apps/widmnth/ChangeLog index 370f41e8a..3fee5e9ec 100644 --- a/apps/widmnth/ChangeLog +++ b/apps/widmnth/ChangeLog @@ -1 +1,2 @@ 0.01: Simple new widget! +0.02: Minor code improvements diff --git a/apps/widmnth/metadata.json b/apps/widmnth/metadata.json index 25f3a8126..d30b58861 100644 --- a/apps/widmnth/metadata.json +++ b/apps/widmnth/metadata.json @@ -1,7 +1,7 @@ { "id": "widmnth", "name": "Days left in month widget", "shortName":"Month Countdown", - "version":"0.01", + "version": "0.02", "description": "A simple widget that displays the number of days left in the month.", "icon": "widget.png", "type": "widget", diff --git a/apps/widmnth/widget.js b/apps/widmnth/widget.js index c4eca155a..472434d2b 100644 --- a/apps/widmnth/widget.js +++ b/apps/widmnth/widget.js @@ -5,7 +5,7 @@ function getDaysLeft(day) { let year = day.getMonth() == 11 ? day.getFullYear() + 1 : day.getFullYear(); // rollover if december. - next_month = new Date(year, (day.getMonth() + 1) % 12, 1, 0, 0, 0); + const next_month = new Date(year, (day.getMonth() + 1) % 12, 1, 0, 0, 0); let days_left = Math.floor((next_month - day) / 86400000); // ms left in month divided by ms in a day return days_left; } diff --git a/apps/widmpsh/ChangeLog b/apps/widmpsh/ChangeLog index e432f82e5..2fa369959 100644 --- a/apps/widmpsh/ChangeLog +++ b/apps/widmpsh/ChangeLog @@ -1 +1,2 @@ 0.01: Copied from widmp and flipped the phase directions! +0.02: Minor code improvements diff --git a/apps/widmpsh/metadata.json b/apps/widmpsh/metadata.json index e6ecf9e73..0ff44a1c8 100644 --- a/apps/widmpsh/metadata.json +++ b/apps/widmpsh/metadata.json @@ -1,7 +1,7 @@ { "id": "widmpsh", "name": "Moon Phase Widget Southern Hemisphere", - "version": "0.01", + "version": "0.02", "description": "Display the current moon phase in blueish for the southern hemisphere in eight phases", "icon": "widget.png", "type": "widget", diff --git a/apps/widmpsh/widget.js b/apps/widmpsh/widget.js index 9115a4719..0ab27001a 100644 --- a/apps/widmpsh/widget.js +++ b/apps/widmpsh/widget.js @@ -1,6 +1,6 @@ WIDGETS["widmoonsh"] = { area: "tr", width: 24, draw: function() { const MC = 29.5305882, NM = 694039.09; - var r = 11, mx = this.x + 12; my = this.y + 12; + var r = 11, mx = this.x + 12, my = this.y + 12; function moonPhase(d) { var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate(); diff --git a/apps/widmsggrid/ChangeLog b/apps/widmsggrid/ChangeLog index 544b4ff4f..0573b9677 100644 --- a/apps/widmsggrid/ChangeLog +++ b/apps/widmsggrid/ChangeLog @@ -3,3 +3,4 @@ 0.03: Use new message library 0.04: Remove library stub 0.05: Don't turn on LCD +0.06: Don't draw outside of widget field diff --git a/apps/widmsggrid/metadata.json b/apps/widmsggrid/metadata.json index 015da533c..75b859c3b 100644 --- a/apps/widmsggrid/metadata.json +++ b/apps/widmsggrid/metadata.json @@ -1,7 +1,7 @@ { "id": "widmsggrid", "name": "Messages Grid Widget", - "version": "0.05", + "version": "0.06", "description": "Widget that displays notification icons in a grid", "icon": "widget.png", "type": "widget", diff --git a/apps/widmsggrid/widget.js b/apps/widmsggrid/widget.js index c2f36a87d..dc444f782 100644 --- a/apps/widmsggrid/widget.js +++ b/apps/widmsggrid/widget.js @@ -52,7 +52,7 @@ if (w.total > 1) { // show total number of messages in bottom-right corner g.reset(); - if (w.total < 10) g.fillCircle(w.x + w.width - 5, w.y + 20, 4); // single digits get a round background, double digits fill their rectangle + if (w.total < 10) g.fillCircle(w.x + w.width - 5, w.y + 19, 4); // single digits get a round background, double digits fill their rectangle g.setColor(g.theme.bg).setBgColor(g.theme.fg) .setFont('6x8').setFontAlign(1, 1) .drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9); diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index e32ab36ba..728c934ea 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ + const Setter = { NONE: "none", UPPER: 'upper', @@ -31,7 +31,7 @@ const upperLshape = isB1 ? { left: 210, bottom: 40, top: 210, - rectWidth: 30, + rectWidth: 30, cornerRoundness: 5, orientation: -1, color: '#f00' @@ -62,7 +62,7 @@ const centerBar = { minY: (upperLshape.bottom + upperLshape.top - (upperLshape.rectWidth*1.5))/2, maxY: (upperLshape.bottom + upperLshape.top + (upperLshape.rectWidth*1.5))/2, confidenceWidth: isB1 ? 10 : 8, - minX: isB1 ? 55 : upperLshape.rectWidth + 14, + minX: isB1 ? 55 : upperLshape.rectWidth + 14, maxX: isB1 ? 165 : Bangle.appRect.x2 - upperLshape.rectWidth - 14 }; diff --git a/apps/worldclock/ChangeLog b/apps/worldclock/ChangeLog index 831dd3b5c..137d53151 100644 --- a/apps/worldclock/ChangeLog +++ b/apps/worldclock/ChangeLog @@ -4,3 +4,6 @@ 0.04: setUI, different screen sizes 0.05: Now update *on* the minute rather than every 15 secs Fix rendering of single extra timezone on Bangle.js 2 +0.06: Minor code improvements +0.07: Minor code improvements +0.08: Add internationalisation of dates \ No newline at end of file diff --git a/apps/worldclock/app.js b/apps/worldclock/app.js index 2627e056c..4207566ec 100644 --- a/apps/worldclock/app.js +++ b/apps/worldclock/app.js @@ -79,10 +79,6 @@ function queueDraw() { }, 60000 - (Date.now() % 60000)); } -function doublenum(x) { - return x < 10 ? "0" + x : "" + x; -} - function getCurrentTimeFromOffset(dt, offset) { return new Date(dt.getTime() + offset * 60 * 60 * 1000); } @@ -90,7 +86,6 @@ function getCurrentTimeFromOffset(dt, offset) { function draw() { // get date var d = new Date(); - var da = d.toString().split(" "); // default draw styles g.reset(); @@ -99,17 +94,17 @@ function draw() { g.setFontAlign(0, 0); // draw time - var time = da[4].substr(0, 5).split(":"); - var hours = time[0], - minutes = time[1]; - + var time = require("locale").time(d, 1); g.setFont(font, primaryTimeFontSize); - g.drawString(`${hours}:${minutes}`, xyCenter, yposTime, true); - // draw Day, name of month, Date - var date = [da[0], da[1], da[2]].join(" "); + const fontHeight = g.getFontHeight(); + g.clearRect(0, yposTime - fontHeight / 2, g.getWidth(), yposTime + fontHeight / 2); + g.drawString(`${time}`, xyCenter, yposTime, true); + var month = require("locale").month(d, 1); + var dayweek = require("locale").dow(d, 1) + var day = d.getDate(); + var date = [dayweek, month, day].join(" "); g.setFont(font, primaryDateFontSize); - g.drawString(date, xyCenter, yposDate, true); // set gmt to UTC+0 @@ -117,15 +112,13 @@ function draw() { // Loop through offset(s) and render offsets.forEach((offset, index) => { - dx = getCurrentTimeFromOffset(gmt, offset[OFFSET_HOURS]); - hours = doublenum(dx.getHours()); - minutes = doublenum(dx.getMinutes()); + const dx = getCurrentTimeFromOffset(gmt, offset[OFFSET_HOURS]); + var time = require("locale").time(dx, 1); if (offsets.length === 1) { // For a single secondary timezone, draw it bigger and drop time zone to second line - const xOffset = 30; g.setFont(font, secondaryTimeFontSize); - g.drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true); + g.drawString(time, xyCenter, yposTime2, true); g.setFont(font, secondaryTimeZoneFontSize); g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime2 + 30, true); @@ -143,7 +136,7 @@ function draw() { true ); g.setFontAlign(1, 0); - g.drawString(`${hours}:${minutes}`, xcol2, yposWorld + index * 15, true); + g.drawString(time, xcol2, yposWorld + index * 15, true); } }); diff --git a/apps/worldclock/metadata.json b/apps/worldclock/metadata.json index 706831a09..587c105af 100644 --- a/apps/worldclock/metadata.json +++ b/apps/worldclock/metadata.json @@ -2,7 +2,7 @@ "id": "worldclock", "name": "World Clock - 4 time zones", "shortName": "World Clock", - "version": "0.05", + "version": "0.08", "description": "Current time zone plus up to four others", "icon": "app.png", "screenshots": [{"url":"screenshot_world.png"}], diff --git a/apps/wristlight/ChangeLog b/apps/wristlight/ChangeLog new file mode 100644 index 000000000..62542be60 --- /dev/null +++ b/apps/wristlight/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Minor code improvements diff --git a/apps/wristlight/app.js b/apps/wristlight/app.js index 24bc1eab2..a0e883c04 100644 --- a/apps/wristlight/app.js +++ b/apps/wristlight/app.js @@ -8,13 +8,13 @@ function draw(color) { } function draw2Pattern() { - colors = ["ff0000", "8080ff", "00ff00", + const colors = ["ff0000", "8080ff", "00ff00", "ffffff"]; drawPattern(2, colors); } function draw3Pattern() { - colors = ["ff0000", "00ff00", "0000ff", + const colors = ["ff0000", "00ff00", "0000ff", "ff00ff", "ffffff", "00ffff", "ffff00", "ff8000", "ff0080"]; drawPattern(3, colors); diff --git a/apps/wristlight/metadata.json b/apps/wristlight/metadata.json index af1d700df..a4970afed 100644 --- a/apps/wristlight/metadata.json +++ b/apps/wristlight/metadata.json @@ -2,7 +2,7 @@ "name": "Wrist Light", "shortName":"Wrist Light", "icon": "wristlight48.png", - "version":"0.01", + "version": "0.02", "description": "A flash light with different colors on your wrist", "tags": "flash,light", "allow_emulator":true, diff --git a/apps/wrkmem/ChangeLog b/apps/wrkmem/ChangeLog index 0684d10ee..2abc811df 100644 --- a/apps/wrkmem/ChangeLog +++ b/apps/wrkmem/ChangeLog @@ -1,2 +1,3 @@ 1.00: Implement Working Memory Helper app -1.01: Fix issue with rendering in 2v18 \ No newline at end of file +1.01: Fix issue with rendering in 2v18 +1.02: Minor code improvements diff --git a/apps/wrkmem/app.js b/apps/wrkmem/app.js index 3f283ca8a..3e085a5ed 100644 --- a/apps/wrkmem/app.js +++ b/apps/wrkmem/app.js @@ -14,7 +14,7 @@ const savedData = { Object.assign(savedData, require("Storage") .readJSON(localTaskFile, true) || {}); -let currentMenu; +//let currentMenu; const allTasks = savedData.tasks; const SWIPE = { @@ -365,7 +365,7 @@ function setMenu(menu) { save(); g.reset(); g.clearRect(Bangle.appRect); - currentMenu = menu; + //currentMenu = menu; menu.render(); menu.setUI(); Bangle.drawWidgets(); diff --git a/apps/wrkmem/metadata.json b/apps/wrkmem/metadata.json index e8860f2c6..4b848bd10 100644 --- a/apps/wrkmem/metadata.json +++ b/apps/wrkmem/metadata.json @@ -2,7 +2,7 @@ "id" : "wrkmem", "name" : "Working Memory Helper", "shortName" : "Work Mem", - "version" : "1.01", + "version": "1.02", "description" : "Externalize your working memory to help stay on task.", "dependencies" : {"textinput": "type"}, "icon" : "icon.png", diff --git a/apps/xxlmessage/ChangeLog b/apps/xxlmessage/ChangeLog index 4c587db52..5bf9a435f 100644 --- a/apps/xxlmessage/ChangeLog +++ b/apps/xxlmessage/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Display icon of the message origin's app \ No newline at end of file +0.02: Display icon of the message origin's app +0.03: Minor code improvements diff --git a/apps/xxlmessage/lib-unbuffered.js b/apps/xxlmessage/lib-unbuffered.js index 68fb2a7a2..36f5392f5 100644 --- a/apps/xxlmessage/lib-unbuffered.js +++ b/apps/xxlmessage/lib-unbuffered.js @@ -66,7 +66,7 @@ var xxl = { }, draw: function() { - wh = 24; // widgets height + const wh = 24; // widgets height var gw = g.getWidth(); var h = (g.getHeight() - wh)/2; // height of drawing area per stripe diff --git a/apps/xxlmessage/metadata.json b/apps/xxlmessage/metadata.json index f8150f0e7..fce3f88cb 100644 --- a/apps/xxlmessage/metadata.json +++ b/apps/xxlmessage/metadata.json @@ -1,7 +1,7 @@ { "id": "xxlmessage", "name": "XXL Message", - "version": "0.02", + "version": "0.03", "shortName": "XXL Msg", "description": "App to display large notifications from iOS and Gadgetbridge/Android", "icon": "app.png", diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 000000000..05ce832cc --- /dev/null +++ b/bin/README.md @@ -0,0 +1,26 @@ +BangleApps Utilities +==================== + +* `sanitycheck.js` - this is run as a CI check (or when `npm test` is used) and checks for common issues with apps or their `metadata.json` +* `create_apps_json.sh` - create the `apps.json` file - this is an aggregation of all `metadata.json` files and is used to speed up loading of BangleApps (or where the server it is hosted on doesn't support directory listing) +* `find_banglejs1_only_apps.sh` - show apps that only work on Bangle.js 1 (and not 2) +* `firmwaremaker_c.js` - create the binary blob needed for the Bangle.js firmware (containing default apps) +* `pre-publish.sh` - this is run before we publish to https://banglejs.com/apps/ - it works out how recently all the apps were updated and writes it to `appdates.csv` +* `font_creator.js` - creates PBF-format fonts for font libraries like `apps/fontsall` + +**You should also check out https://github.com/espruino/EspruinoAppLoaderCore/tree/master/tools** (available in `core/tools` in this repo) - this contains tools for handling languages, as well as a command-line based app loader + +Related to Linting code: + +* `bulk-update-apps.mjs` - use this script to bump the version of many apps with the same changes +* `exempt-lint.mjs` - exempt an app file from a specific eslint rule +* `sync-lint-exemptions.mjs` - Run this to ensure that the lint exemptions are all valid. If any of the exempt app files have been changed, this script will remove the exemption for that file. + +Prototypes: + +* `runapptests.js` - **PROTOTYPE** - runs tests for apps (where defined) in an emulator so apps can be tested offline +* `thumbnailer.js` - **PROTOTYPE** - runs all apps in an emulator and automatically outputs thumbnails for them + +Legacy: + +* `firmwaremaker.js` - **LEGACY** create a JS file containing all the commands needed to write firmware to a Bangle. Was used for Bangle.js 1 factory programming diff --git a/bin/bulk-update-apps.mjs b/bin/bulk-update-apps.mjs new file mode 100644 index 000000000..fda2fa283 --- /dev/null +++ b/bin/bulk-update-apps.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +/** + * @file + * You can use this script to bump the version of many apps with the same changes. + * You need to specify a hash for a git commit along with a message that describes the changes made to the apps. + * + * The apps that were changed in the git commit are bumped to a new minor version, + * and their changelogs are updated with the message you provided. + * + * Run it like this: + * node bin/bulk-update-apps.mjs GITHASH CHANGELOGMESSAGE + * + * Example command: + * node bin/bulk-update-apps.mjs 29ced17i7 'Minor code improvements' + * + * You can also run it in output mode like this: + * node bin/bulk-update-apps.mjs GITHASH --output + * + * This mode doesn't make any changes to your files, it outputs the ID's of all apps that would be bumped. + */ + +import { exec } from "node:child_process"; +import fs from "node:fs/promises"; + +const commitHash = process.argv[2]; +if (!commitHash || commitHash === "--output") { + throw new Error( + "First argument needs to be a git commit hash, something like '29ced17i7'", + ); +} + +const changelogMessage = process.argv[3]; +if (!changelogMessage) { + throw new Error( + "Second argument needs to be a changelog message, something like 'Minor code improvements'", + ); +} + +let outputFlag = false; +if (process.argv[3] === "--output" || process.argv[4] === "--output") { + outputFlag = true; +} + +const gitOutput = await new Promise((resolve) => { + exec( + `git diff-tree --no-commit-id --name-only ${commitHash} -r`, + (error, stdout, stderr) => { + if (error) { + throw new Error(`Could not get git diff: ${error}`); + } else if (stderr) { + throw new Error(`Could not get git diff: ${stderr}`); + } else if (!stdout) { + throw new Error(`Git command did not return any data`); + } + + resolve(stdout); + }, + ); +}); + +/** + * Extract the id of each app and make sure there are no duplicates + */ +const appIds = [ + ...new Set( + [...gitOutput.matchAll(/^(?:.*?\/apps\/|apps\/)(?.*?)\//gmu)] + .map((match) => match?.groups?.id) + .filter((match) => match), + ), +]; + +if (outputFlag) { + for (const appId of appIds) { + console.log(appId); + } +} else { + for (const appId of appIds) { + const metadataPath = `apps/${appId}/metadata.json`; + const changelogPath = `apps/${appId}/ChangeLog`; + + const metadataContent = await fs.readFile(metadataPath, { + encoding: "utf8", + }); + const metadata = JSON.parse(metadataContent); + + const minorVersionNumber = parseInt(metadata.version.split(".").at(-1)); + const newMinorVersionString = `${minorVersionNumber < 9 ? "0" : ""}${(minorVersionNumber + 1).toString()}`; + const newVersion = [ + ...metadata.version.split(".").slice(0, -1), + newMinorVersionString, + ].join("."); + + const versionMatch = metadataContent.match( + /^\s*(?"version"\s*:\s*".*"\s*,)/mu, + ); + const newMatadataContent = metadataContent.replace( + versionMatch.groups.declaration, + `"version": "${newVersion}",`, + ); + await fs.writeFile(metadataPath, newMatadataContent); + + let changelog = await fs.readFile(changelogPath, { + encoding: "utf8", + flag: "a+", + }); + + changelog = changelog.replace(/\n*$/u, ""); //trim trailing whitespace + changelog ||= "0.01: New app!"; // init the changelog file if it doesn't exist + changelog += `\n${newVersion}: ${changelogMessage}\n`; // add the new version with correct whitespace + + await fs.writeFile(changelogPath, changelog, { flag: "w+" }); + } +} diff --git a/bin/create_app_supports_field.js b/bin/create_app_supports_field.js deleted file mode 100644 index 24d6694f2..000000000 --- a/bin/create_app_supports_field.js +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env nodejs -/* Quick hack to add proper 'supports' field to apps.json -*/ - -var fs = require("fs"); - -var BASEDIR = __dirname+"/../"; - -var appsFile, apps; -try { - appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); -} catch (e) { - ERROR("apps.json not found"); -} -try{ - apps = JSON.parse(appsFile); -} catch (e) { - console.log(e); - var m = e.toString().match(/in JSON at position (\d+)/); - if (m) { - var char = parseInt(m[1]); - console.log("==============================================="); - console.log("LINE "+appsFile.substr(0,char).split("\n").length); - console.log("==============================================="); - console.log(appsFile.substr(char-10, 20)); - console.log("==============================================="); - } - console.log(m); - ERROR("apps.json not valid JSON"); - -} - -apps = apps.map((app,appIdx) => { - if (app.supports) return app; // already sorted - var tags = []; - if (app.tags) tags = app.tags.split(",").map(t=>t.trim()); - var supportsB1 = true; - var supportsB2 = false; - if (tags.includes("b2")) { - tags = tags.filter(x=>x!="b2"); - supportsB2 = true; - } - if (tags.includes("bno2")) { - tags = tags.filter(x=>x!="bno2"); - supportsB2 = false; - } - if (tags.includes("bno1")) { - tags = tags.filter(x=>x!="bno1"); - supportsB1 = false; - } - app.tags = tags.join(","); - app.supports = []; - if (supportsB1) app.supports.push("BANGLEJS"); - if (supportsB2) app.supports.push("BANGLEJS2"); - return app; -}); - -// search for screenshots -apps = apps.map((app,appIdx) => { - if (app.screenshots) return app; // already sorted - - var files = require("fs").readdirSync(__dirname+"/../apps/"+app.id); - var screenshots = files.filter(fn=>fn.startsWith("screenshot") && fn.endsWith(".png")); - if (screenshots.length) - app.screenshots = screenshots.map(fn => ({url:fn})); - return app; -}); - -var KEY_ORDER = [ - "id","name","shortName","version","description","icon","screenshots","type","tags","supports", - "dependencies", "readme", "custom", "customConnect", "interface", - "allow_emulator", "storage", "data", "sortorder" -]; - -var JS = JSON.stringify; -var json = "[\n "+apps.map(app=>{ - var keys = KEY_ORDER.filter(k=>k in app); - Object.keys(app).forEach(k=>{ - if (!KEY_ORDER.includes(k)) - throw new Error(`Key named ${k} not known!`); - }); - //var keys = Object.keys(app); // don't re-order - - return "{\n "+keys.map(k=>{ - var js = JS(app[k]); - if (k=="storage") { - if (app.storage.length) - js = "[\n "+app.storage.map(s=>JS(s)).join(",\n ")+"\n ]"; - else - js = "[]"; - } - return JS(k)+": "+js; - }).join(",\n ")+"\n }"; -}).join(",\n ")+"\n]\n"; - -//console.log(json); - -console.log("new apps.json written"); -fs.writeFileSync(BASEDIR+"apps.json", json); diff --git a/bin/exempt-lint.mjs b/bin/exempt-lint.mjs new file mode 100755 index 000000000..eb590a533 --- /dev/null +++ b/bin/exempt-lint.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +/** + * @file + * You can use this script to exempt an app file from a specific eslint rule. + * + * This should only be used to exempt existing apps when a new lint rule is added. + * You are not allowed to exempt your new app from existing lint rules. + * + * Run it like this: + * node bin/exempt-lint.mjs LINTRULE FILEPATH + * + * Example command: + * node bin/exempt-lint.mjs no-unused-vars ./apps/_example_app/app.js + */ + +import fs from "node:fs/promises"; + +// Nodejs v18 compatibility (v18 is end-of-life in april 2025) +if(!("crypto" in globalThis)) globalThis.crypto = (await import("node:crypto")).webcrypto; + +const lintRule = process.argv[2]; +if (!lintRule) { + throw new Error( + "First argument needs to be a lint rule, something like 'no-unused-vars'", + ); +} + +const filePathInput = process.argv[3]; +const filePathMatch = filePathInput?.match( + /^(?:.*?\/apps\/|apps\/|\/)?(?.*\.[jt]s)$/iu, +); +const filePath = filePathMatch?.groups?.path; +if (!filePath) { + throw new Error( + "Second argument needs to be a file path that looks something like './apps/_example_app/app.js'", + ); +} + +const exemptionsFilePath = "../apps/lint_exemptions.js"; + +const exemptions = (await import(exemptionsFilePath)).default; + +const fileContents = await fs.readFile(`apps/${filePath}`, "utf8"); + +const exemption = exemptions[filePath] || {}; +exemption.hash = await hashContents(fileContents); +const rules = new Set(exemption.rules || []); +rules.add(lintRule); +exemption.rules = [...rules]; +exemptions[filePath] = exemption; + +const output = `module.exports = ${JSON.stringify(exemptions, undefined, 2)};\n`; +await fs.writeFile(`bin/${exemptionsFilePath}`, output); + +console.log(`✔️ '${filePath}' is now exempt from the rule '${lintRule}'`); + +/** + * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string + */ +async function hashContents(message) { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); // convert bytes to hex string + return hashHex; +} diff --git a/bin/find_banglejs1_only_apps.sh b/bin/find_banglejs1_only_apps.sh new file mode 100755 index 000000000..d34dbc72b --- /dev/null +++ b/bin/find_banglejs1_only_apps.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd `dirname $0`/.. +find apps -name metadata.json | xargs -I {} grep '\["BANGLEJS"\]' -A 100 -B 100 {} diff --git a/bin/font_creator.js b/bin/font_creator.js new file mode 100755 index 000000000..352c7ea24 --- /dev/null +++ b/bin/font_creator.js @@ -0,0 +1,75 @@ +#!/usr/bin/node +// Used for apps/fontsall/etc +// Needs 'npm install pngjs' + +var FONTFILE = "unifont-15.1.05.png"; + +console.log("Espruino Font Creator"); +console.log("---------------------"); +console.log(""); + +let fontconverter = require("../webtools/fontconverter.js"); +let charCodeRanges = fontconverter.getRanges(); +console.log("Available char code ranges\n - "+Object.keys(charCodeRanges).join("\n - ")); + + +if (process.argv.length!=4) { + console.log(process.argv,""); + console.log("USAGE:"); + console.log(" font_creator 'CharCodeRange' outputfile.pbf"); + process.exit(1); +} +let charCodeRange = process.argv[2]; +let outputFile = process.argv[3]; + +if (!(charCodeRange in charCodeRanges)) { + console.log("Char code range "+charCodeRange+" not found"); + process.exit(1); +} + +if (!require("fs").existsSync(FONTFILE)) { + console.log("Unifont file "+FONTFILE+" not found!") + console.log("Download from https://unifoundry.com/unifont/index.html and convert to png") + process.exit(1); +} + +// load a unifont PNG file +let font = fontconverter.load({ + fn : FONTFILE, + mapWidth : 256, mapHeight : 256, + mapOffsetX : 32, mapOffsetY : 64, + height : 16, // actual used height of font map + range : charCodeRanges[charCodeRange].range +}); +/*let font = fontconverter.load({ + fn : "fontname.bdf", +});*/ +font.removeUnifontPlaceholders(); +// quick hack as space looks too long +font.glyphs[32].width -= 4; +font.glyphs[32].xEnd -= 4; +font.glyphs[32].advance -= 4; + +/* Another hack - because these are centered in the image +they often don't start at the beginning of the char space. +Move them all back and add 1px at the end */ +font.glyphs.forEach(g => { + if (g.xStart>0) { + var shift = g.xStart; + g.xStart -= shift; + g.xEnd -= shift; + g.advance = g.xEnd+2; + g.oldGetPixel = g.getPixel; + g.getPixel = (x,y) => g.oldGetPixel(x+shift,y); + //g.debug(); + //console.log(g); + console.log(); + } +}); + +/*var g = font.glyphs[":".charCodeAt()]; +g.debug(); +console.log(g);*/ +font.debugChars(); +require("fs").writeFileSync(outputFile, Buffer.from(font.getPBF())); +//console.log(font.getJS()); diff --git a/bin/runapptests.js b/bin/runapptests.js index 40a898fa6..dcbf13c58 100755 --- a/bin/runapptests.js +++ b/bin/runapptests.js @@ -8,6 +8,8 @@ IT IS UNFINISHED It searches for `test.json` in each app's directory and will run them in sequence. +The return code is the number of failed tests. + TODO: * more code to test with @@ -20,32 +22,54 @@ TODO: */ -// A simpletest -/*var TEST = { - app : "android", - tests : [ { - steps : [ - {t:"load", fn:"messagesgui.app.js"}, - {t:"gb", "obj":{"t":"notify","id":1234,"src":"Twitter","title":"A Name","body":"message contents"}}, - {t:"cmd", "js":"X='hello';"}, - {t:"eval", "js":"X", eq:"hello"} - ] - }] -};*/ -var TEST = { - app : "antonclk", - tests : [ { - steps : [ - {t:"cmd", "js": "Bangle.loadWidgets()"}, - {t:"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, - {t:"cmd", "js":"Bangle.setUI()"}, // load and free - {t:"saveMemoryUsage"}, - {t:"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, - {t:"cmd", "js":"Bangle.setUI()"}, // load and free - {t:"checkMemoryUsage"}, // check memory usage is the same - ] - }] +const DEMOAPP = { + "id":"demoappfortestformat", + "name":"demo", + "version":"0.01", + "type":"app", + "supports":["BANGLEJS2"], + "storage":[], }; +const DEMOTEST = { + "app" : "demoappfortestformat", + "setup" : [{ + "id": "arbitraryid", + "steps" : [ + {"t":"cmd", "js": "global.testfunction = ()=>{}", "text": "Runs some code on the device"}, + {"t":"wrap", "fn": "global.testfunction", "id": "testfunc", text:"Wraps a function to count calls and store the last set of arguments on the device"} + ] + }], + "tests" : [{ + "description": "Optional description of the test, will be shown in results table", + "steps" : [ + {"t":"setup", "id": "arbitraryid", "text": "Calls a set of predefined steps"}, + {"t":"eval", "js": "'test' + 'value'", "eq": "testvalue", "text": "Evals code on the device and compares the resulting string to the value in 'eq'"}, +// {"t":"console", "text": "Starts an interactive console for debugging"}, + {"t":"saveMemoryUsage", "text": "Gets and stores the current memory usage"}, + {"t":"checkMemoryUsage", "text": "Checks the current memory to be equal to the stored value"}, + {"t":"assert", "js": "0", "is":"falsy", "text": "Evaluates the content of 'js' on the device and asserts if the result is falsy"}, + {"t":"assert", "js": "1", "is":"truthy", "text": "Evaluates the content of 'js' on the device and asserts if the result is truthy"}, + {"t":"assert", "js": "false", "is":"false", "text": "Evaluates the content of 'js' on the device and asserts if the result is false"}, + {"t":"assert", "js": "true", "is":"true", "text": "Evaluates the content of 'js' on the device and asserts if the result is true"}, + {"t":"assert", "js": "()=>{}", "is":"function", "text": "Evaluates the content of 'js' and on the device and asserts if the result is a function"}, + {"t":"assert", "js": "123", "is":"equal", "to": "123", "text": "Evaluates the content of 'js' and 'to' on the device and asserts if the result is equal"}, + {"t":"assertArray", "js": "[]", "is":"undefinedOrEmpty", "text": "Evaluates the content of 'js' on the device and asserts if the result is undefined or an empty array"}, + {"t":"assertArray", "js": "[1,2,3]", "is":"notEmpty", "text": "Evaluates the content of 'js' on the device and asserts if the result is an array with more than 0 entries"}, + {"t":"cmd", "js": "global.testfunction(1)", "text": "Call function for the following asserts"}, + {"t":"assertCall", "id": "testfunc", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ] , "text": "Asserts if a wrapped function has been called with the expected arguments"}, + {"t":"resetCall", "id": "testfunc", "text": "Reset the recorded calls"}, + {"t":"assertCall", "id": "testfunc", "count": 0 , "text": "Asserts if a wrapped function has been called the expected number of times"} + ] + }, { + "description": "Emulator timers and intervals can advanced by a given time", + "steps": [ + {"t":"cmd", "js":"setTimeout(()=>{global.waited = true},60000)", "text": "Set a timeout for 60 seconds"}, + {"t":"assert", "js":"global.waited", "is": "falsy", "text": "Timeout has not yet fired"}, + {"t":"advanceTimers", "ms":60000, "text": "Advance timers by 60000 ms to get the timer to fire in the next idle period"}, + {"t":"assert", "js":"global.waited", "is": "true", "text": "Timeout has fired"} + ] + }] +} var EMULATOR = "banglejs2"; var DEVICEID = "BANGLEJS2"; @@ -62,6 +86,9 @@ if (!require("fs").existsSync(DIR_IDE)) { process.exit(1); } +const verbose = process.argv.includes("--verbose") || process.argv.includes("-v"); + +var AppInfo = require(BASE_DIR+"/core/js/appinfo.js"); var apploader = require(BASE_DIR+"/core/lib/apploader.js"); apploader.init({ DEVICEID : DEVICEID @@ -71,97 +98,412 @@ var emu = require(BASE_DIR+"/core/lib/emulator.js"); // Last set of text received var lastTxt; +function getSanitizedLastLine(){ + return emu.getLastLine().replaceAll("\r", ""); +} + function ERROR(s) { console.error(s); process.exit(1); } -function runTest(test) { +function getValue(js){ + if(verbose) + console.log(`> GETTING VALUE FOR \`${js}\``); + emu.tx(`\x10print(JSON.stringify(${js}))\n`); + var result = getSanitizedLastLine(); + + if (verbose) + console.log(` GOT \`${result}\``); + return JSON.parse(result); +} + +function assertArray(step){ + console.log(`> ASSERT ARRAY ${step.js} IS`,step.is.toUpperCase(), step.text ? "- " + step.text : ""); + let isOK; + switch (step.is.toLowerCase()){ + case "notempty": isOK = getValue(`${step.js} && ${step.js}.length > 0`); break; + case "undefinedorempty": isOK = getValue(`!${step.js} || (${step.js} && ${step.js}.length === 0)`); break; + } + + if (isOK) { + if (verbose) + console.log("> OK -", `\`${step.js}\``); + } else + console.log("> FAIL -", `\`${step.js}\``); + return isOK; +} + +function assertValue(step){ + console.log("> ASSERT " + `\`${step.js}\``, "IS", step.is.toUpperCase() + (step.to !== undefined ? " TO " + `\`${step.to}\`` : ""), step.text ? "- " + step.text : ""); + let isOK; + switch (step.is.toLowerCase()){ + case "truthy": isOK = getValue(`!!${step.js}`); break; + case "falsy": isOK = getValue(`!${step.js}`); break; + case "true": isOK = getValue(`${step.js} === true`); break; + case "false": isOK = getValue(`${step.js} === false`); break; + case "equal": isOK = getValue(`${step.js} == ${step.to}`); break; + case "function": isOK = getValue(`typeof ${step.js} === "function"`); break; + } + + if (isOK){ + if (verbose) + console.log("> OK - " + `\`${step.js}\``, "IS", step.is.toUpperCase(), step.to ? "TO " + `\`${step.js}\`` : ""); + } else + console.log("> FAIL - " + `\`${step.js}\``, "IS", step.is.toUpperCase(), step.to ? "TO " + `\`${step.js}\`` : ""); + return isOK; +} + +function wrap(func, id){ + console.log(`> WRAPPING \`${func}\` AS ${id}`); + + let wrappingCode = ` + if(!global.APPTESTS) global.APPTESTS={}; + if(!global.APPTESTS.funcCalls) global.APPTESTS.funcCalls={}; + if(!global.APPTESTS.funcArgs) global.APPTESTS.funcArgs={}; + global.APPTESTS.funcCalls.${id}=0; + (function(o) { + ${func} = function() { + global.APPTESTS.funcCalls.${id}++; + global.APPTESTS.funcArgs.${id}=arguments; + return o.apply(this, arguments); + }; + }(${func}));\n`; + + emu.tx(wrappingCode); +} + +function assertCall(step){ + console.log("> ASSERT CALL", step.id, step.text ? "- " + step.text : ""); + let isOK = true; + let id = step.id; + let args = step.argAsserts; + if (step.count !== undefined){ + let calls = getValue(`global.APPTESTS.funcCalls.${id}`); + isOK = step.count == calls + } + if (args && args.length > 0){ + let callArgs = getValue(`global.APPTESTS.funcArgs.${id}`); + for (let a of args){ + let current = { + js: callArgs[a.arg], + is: a.is, + to: a.to, + text: step.text + }; + switch(a.t){ + case "assertArray": + isOK = isOK && assertArray(current); + break; + case "assert": + isOK = isOK && assertValue(current); + break; + } + } + } + if (isOK){ + if (verbose) + console.log("OK - ASSERT CALL", step.text ? "- " + step.text : ""); + } else + console.log("FAIL - ASSERT CALL", step.text ? "- " + step.text : ""); + return isOK; +} + +function runStep(step, subtest, test, state){ + let p = Promise.resolve(); + if (state.ok) switch(step.t) { + case "setup" : + test.setup.filter(e=>e.id==step.id)[0].steps.forEach(setupStep=>{ + p = p.then(()=>{ + let np = runStep(setupStep, subtest, test, state); + emu.idle(); + return np; + }); + }); + break; + case "load" : + p = p.then(() => { + console.log(`> LOADING FILE "${step.fn}"`); + emu.tx(`load(${JSON.stringify(step.fn)})\n`); + }); + break; + case "cmd" : + p = p.then(() => { + console.log(`> SENDING JS \`${step.js}\``, step.text ? "- " + step.text : ""); + emu.tx(`${step.js}\n`); + }); + break; + case "wrap" : + p = p.then(() => { + wrap(step.fn, step.id); + }); + break; + case "gb" : + p = p.then(() => { + let obj = Object.assign({ + src:'Messenger', + t: 'notify', + type: 'text', + id: Date.now().toFixed(0), + title:'title', + body:'body' + }, step.obj || {}); + console.log(`> GB with`, verbose ? "event " + JSON.stringify(obj, null, null) : "type " + obj.t); + emu.tx(`GB(${JSON.stringify(obj)})\n`); + }); + break; + case "emit" : + p = p.then(() => { + let parent = step.parent ? step.parent : "Bangle"; + if (!step.paramsArray) step.paramsArray = []; + let args = JSON.stringify([step.event].concat(step.paramsArray)); + console.log(`> EMIT "${step.event}" on ${parent} with parameters ${JSON.stringify(step.paramsArray, null, null)}`); + + emu.tx(`${parent}.emit.apply(${parent}, ${args})\n`); + }); + break; + case "eval" : + p = p.then(() => { + console.log(`> EVAL \`${step.js}\``); + emu.tx(`\x10print(JSON.stringify(${step.js}))\n`); + var result = getSanitizedLastLine(); + var expected = JSON.stringify(step.eq); + if (verbose) + console.log("> GOT `"+result+"`"); + if (result!=expected) { + console.log("> FAIL: EXPECTED "+expected); + state.ok = false; + } else if (verbose) { + console.log("> OK: EXPECTED "+expected); + } + }); + break; + // tap/touch/drag/button press + // delay X milliseconds? + case "assertArray": + p = p.then(() => { + state.ok &= assertArray(step); + }); + break; + case "resetCall": + console.log(`> RESET CALL ${step.id}`, step.text ? "- " + step.text : ""); + emu.tx(`global.APPTESTS.funcCalls.${step.id} = 0;\n`); + emu.tx(`global.APPTESTS.funcArgs.${step.id} = undefined;\n`); + break; + case "assertCall": + p = p.then(() => { + state.ok &= assertCall(step); + }); + break; + case "assert": + p = p.then(() => { + state.ok &= assertValue(step); + }); + break; + case "screenshot" : + p = p.then(() => { + console.log(`> Compare screenshots - UNIMPLEMENTED`); + }); + break; + case "saveMemoryUsage" : + p = p.then(() => { + emu.tx(`\x10print(process.memory().usage)\n`); + subtest.memUsage = parseInt(getSanitizedLastLine()); + console.log("> SAVED MEMORY USAGE", subtest.memUsage); + }); + break; + case "checkMemoryUsage" : + p = p.then(() => { + emu.tx(`\x10print(process.memory().usage)\n`); + var memUsage = parseInt(getSanitizedLastLine()); + console.log("> COMPARE MEMORY USAGE", memUsage); + if (subtest.memUsage != memUsage ) { + console.log("> FAIL: EXPECTED MEMORY USAGE OF "+subtest.memUsage); + state.ok = false; + } + }); + break; + case "advanceTimers" : + p = p.then(()=>{ + console.log("> ADVANCE TIMERS BY", step.ms + "ms"); + emu.tx(`for(let c of global["\xff"].timers){ + if(c) c.time -= ${step.ms * 1000}; + }\n`); + }); + break; + case "upload" : + p = p.then(()=>{ + console.log("> UPLOADING" + (step.load ? " AND LOADING" : ""), step.file); + emu.tx(AppInfo.getFileUploadCommands(step.as, require("fs").readFileSync(BASE_DIR + "/" + step.file).toString())); + if (step.load){ + emu.tx(`\x10load("${step.as}")\n`); + } + }); + break; + case "console" : + p = p.then(()=>{ + return new Promise(resolve => { + if (process.stdin.isTTY){ + console.log("> STARTING INTERACTIVE CONSOLE"); + + let shutdownHandler = function (code) { + console.log(" STOPPING INTERACTIVE CONSOLE"); + process.stdin.removeListener("readable", stdinlistener) + process.stdin.setRawMode(false); + handleRx = ()=>{}; + handleConsoleOutput = handleConsoleOutputCurrent; + resolve(); + } + + let stdinlistener = () => { + while ((chunk = process.stdin.read()) !== null) { + if (chunk === '\x03') { + shutdownHandler(); + } + emu.tx(chunk.toString()); + } + }; + + handleRx = (c) => { + process.stdout.write(String.fromCharCode(c)); + } + + let handleConsoleOutputCurrent = handleConsoleOutput; + handleConsoleOutput = () => {}; + + process.stdin.setRawMode(true); + process.stdin.setEncoding('ASCII'); + process.stdin.on("readable", stdinlistener); + + process.stdout.write(">"); + } else { + console.log("> TERMINAL NEEDS TO BE A TTY FOR INTERACTIVE CONSOLE"); + resolve(); + } + }) + }); + break; + default: ERROR("Unknown step type "+step.t); + } + p = p.then(()=> { + emu.idle(); + }); + return p; +} + +function runTest(test, testState) { + apploader.reset(); var app = apploader.apps.find(a=>a.id==test.app); if (!app) ERROR(`App ${JSON.stringify(test.app)} not found`); if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); + return apploader.getAppFilesString(app).then(command => { - // What about dependencies?? + let p = Promise.resolve(); test.tests.forEach((subtest,subtestIdx) => { - console.log(`==============================`); - console.log(`"${test.app}" Test ${subtestIdx}`); - console.log(`==============================`); - emu.factoryReset(); - console.log("> Sending app "+test.app); - emu.tx(command); - console.log("> Sent app"); - emu.tx("reset()\n"); - console.log("> Reset."); - var ok = true; + let state = { ok: true}; + p = p.then(()=>{ + console.log(`==============================`); + console.log(`"${test.app}" Test ${subtestIdx}`); + if (test.description) + console.log(`"${test.description}`); + console.log(`==============================`); + emu.factoryReset(); + console.log("> SENDING APP "+test.app); + emu.tx(command); + if (verbose) + console.log("> SENT APP"); + emu.tx("reset()\n"); + console.log("> RESET"); + + }); + subtest.steps.forEach(step => { - if (ok) switch(step.t) { - case "load" : - console.log(`> Loading file "${step.fn}"`); - emu.tx(`load(${JSON.stringify(step.fn)})\n`); - break; - case "cmd" : - console.log(`> Sending JS "${step.js}"`); - emu.tx(`${step.js}\n`); - break; - case "gb" : emu.tx(`GB(${JSON.stringify(step.obj)})\n`); break; - case "tap" : emu.tx(`Bangle.emit(...)\n`); break; - case "eval" : - console.log(`> Evaluate "${step.js}"`); - emu.tx(`\x10print(JSON.stringify(${step.js}))\n`); - var result = emu.getLastLine(); - var expected = JSON.stringify(step.eq); - console.log("> GOT "+result); - if (result!=expected) { - console.log("> FAIL: EXPECTED "+expected); - ok = false; - } - break; - // tap/touch/drag/button press - // delay X milliseconds? - case "screenshot" : - console.log(`> Compare screenshots - UNIMPLEMENTED`); - break; - case "saveMemoryUsage" : - emu.tx(`\x10print(process.memory().usage)\n`); - subtest.memUsage = parseInt( emu.getLastLine()); - console.log("> CURRENT MEMORY USAGE", subtest.memUsage); - break; - case "checkMemoryUsage" : - emu.tx(`\x10print(process.memory().usage)\n`); - var memUsage = emu.getLastLine(); - console.log("> CURRENT MEMORY USAGE", memUsage); - if (subtest.memUsage != memUsage ) { - console.log("> FAIL: EXPECTED MEMORY USAGE OF "+subtest.memUsage); - ok = false; - } - break; - default: ERROR("Unknown step type "+step.t); - } - emu.idle(); + p = p.then(()=>{ + return runStep(step, subtest, test, state).catch((e)=>{ + console.log("> STEP FAILED:", e, step); + state.ok = false; + }) + }); + }); + + p = p.finally(()=>{ + console.log("> RESULT -", (state.ok ? "OK": "FAIL") , "- " + test.app + (subtest.description ? (" - " + subtest.description) : "")); + testState.push({ + app: test.app, + number: subtestIdx, + result: state.ok ? "SUCCESS": "FAILURE", + description: subtest.description + }); }); }); - emu.stopIdle(); + p = p.then(()=>{ + emu.stopIdle(); + }); + return p; }); } +let handleRx = ()=>{}; +let handleConsoleOutput = () => {}; +if (verbose){ + handleConsoleOutput = (d) => { + console.log("<", d); + } +} + +let testState = []; + emu.init({ EMULATOR : EMULATOR, - DEVICEID : DEVICEID + DEVICEID : DEVICEID, + rxCallback : (ch)=>{ + handleRx(ch); + }, + consoleOutputCallback: (d)=>{ + handleConsoleOutput(d); + } }).then(function() { // Emulator is now loaded console.log("Loading tests"); - var tests = []; - apploader.apps.forEach(app => { - var testFile = APP_DIR+"/"+app.id+"/test.json"; - if (!require("fs").existsSync(testFile)) return; - var test = JSON.parse(require("fs").readFileSync(testFile).toString()); - test.app = app.id; - tests.push(test); + let p = Promise.resolve(); + let apps = apploader.apps; + + apps.push(DEMOAPP); + + if (process.argv.includes("--id")) { + let f = process.argv[process.argv.indexOf("--id") + 1]; + apps = apps.filter(e=>e.id==f); + if (apps.length == 0){ + console.log("No apps left after filtering for " + f); + process.exitCode(255); + } + } + + apps.forEach(app => { + let test = DEMOTEST; + if (app.id != DEMOAPP.id){ + let testFile = APP_DIR+"/"+app.id+"/test.json"; + if (!require("fs").existsSync(testFile)) return; + test = JSON.parse(require("fs").readFileSync(testFile).toString()); + test.app = app.id; + } + p = p.then(()=>{ + return runTest(test, testState); + }); }); - // Running tests - runTest(TEST); + p.finally(()=>{ + console.log("\n\n"); + console.log("Overall results:"); + console.table(testState); + + process.exit(testState.reduce((a,c)=>{ + return a || ((c.result == "SUCCESS") ? 0 : 1); + }, 0)) + }); + return p; }); + /* if (erroredApps.length) { erroredApps.forEach(app => { diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 78cadc34c..041540606 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -19,6 +19,7 @@ try { var BASEDIR = __dirname+"/../"; var APPSDIR_RELATIVE = "apps/"; var APPSDIR = BASEDIR + APPSDIR_RELATIVE; +var knownWarningCount = 0; var warningCount = 0; var errorCount = 0; function ERROR(msg, opt) { @@ -32,10 +33,11 @@ function WARN(msg, opt) { opt = opt||{}; if (KNOWN_WARNINGS.includes(msg)) { console.log(`Known warning : ${msg}`); + knownWarningCount++; } else { console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + warningCount++; } - warningCount++; } var apps = []; @@ -150,10 +152,13 @@ apps.forEach((app,appIdx) => { } else { var changeLog = fs.readFileSync(appDir+"ChangeLog").toString(); var versions = changeLog.match(/\d+\.\d+:/g); - if (!versions) ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`, {file:metadataFile}); - var lastChangeLog = versions.pop().slice(0,-1); - if (lastChangeLog != app.version) - ERROR(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`, {file:appDirRelative+"ChangeLog", line:changeLog.split("\n").length-1}); + if (!versions) { + ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`, {file:metadataFile}); + } else { + var lastChangeLog = versions.pop().slice(0,-1); + if (lastChangeLog != app.version) + ERROR(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`, {file:appDirRelative+"ChangeLog", line:changeLog.split("\n").length-1}); + } } } if (!app.description) ERROR(`App ${app.id} has no description`, {file:metadataFile}); @@ -393,8 +398,11 @@ while(fileA=allFiles.pop()) { } console.log("=================================="); -console.log(`${errorCount} errors, ${warningCount} warnings`); +console.log(`${errorCount} errors, ${warningCount} warnings (and ${knownWarningCount} known warnings)`); console.log("=================================="); if (errorCount) { process.exit(1); +} else if ("CI" in process.env && warningCount) { + console.log("Running in CI, raising an error from warnings"); + process.exit(1); } diff --git a/bin/sync-lint-exemptions.mjs b/bin/sync-lint-exemptions.mjs new file mode 100755 index 000000000..84e295da6 --- /dev/null +++ b/bin/sync-lint-exemptions.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/** + * @file + * Run this to ensure that the lint exemptions are all valid. + * If any of the exempt app files have been changed, this script will remove the exemption for that file. + * + * Run it like this: + * node bin/sync-lint-exemptions.mjs + */ + +import fs from "node:fs/promises"; + +// Nodejs v18 compatibility (v18 is end-of-life in april 2025) +if(!("crypto" in globalThis)) globalThis.crypto = (await import("node:crypto")).webcrypto; + +const exemptionsFilePath = "../apps/lint_exemptions.js"; + +const exemptions = (await import(exemptionsFilePath)).default; + +for (const filePath of Object.keys(exemptions)) { + const fileContents = await fs.readFile(filePath, "utf8"); + const currentHash = await hashContents(fileContents); + if (exemptions[filePath].hash !== currentHash) { + delete exemptions[filePath]; + console.log( + `! Removed lint exemptions for '${filePath}' because it has been edited`, + ); + } +} + +const output = `module.exports = ${JSON.stringify(exemptions, undefined, 2)};\n`; +await fs.writeFile("bin/" + exemptionsFilePath, output); + +console.log(`✔️ Synchronized all lint exemptions\n`); + +/** + * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string + */ +async function hashContents(message) { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); // convert bytes to hex string + return hashHex; +} diff --git a/core b/core index bd301be33..1cdcb3405 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit bd301be3324775a8f464328ba9e34f750d503a2b +Subproject commit 1cdcb3405f78ef35f231b9c3df501721bda75525 diff --git a/css/main.css b/css/main.css index e8330b868..810b9a032 100644 --- a/css/main.css +++ b/css/main.css @@ -113,7 +113,7 @@ a.btn.btn-link.dropdown-toggle { font-size: 50%; color : #F66; position:relative; - top:-0.7em; + top:-0.8em; } .icon.icon-favourite-active span { color : white; diff --git a/img/banglejs-logo-small.svg b/img/banglejs-logo-small.svg new file mode 100644 index 000000000..16ef3c2c7 --- /dev/null +++ b/img/banglejs-logo-small.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/img/banglejs-logo-sml.png b/img/banglejs-logo-sml.png deleted file mode 100644 index c599f042a..000000000 Binary files a/img/banglejs-logo-sml.png and /dev/null differ diff --git a/index.html b/index.html index 655a389e5..c8c34db66 100644 --- a/index.html +++ b/index.html @@ -22,7 +22,7 @@